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 #include "abstract_output.h"
11 #include "platform.h"
12 #include "abstract_client.h"
13 #include "cursor.h"
14 #include "deleted.h"
15 #include "effects.h"
16 #include "pointer_input.h"
17 #include "options.h"
18 #include "screenedge.h"
19 #include "screens.h"
20 #include "virtualdesktops.h"
21 #include "wayland_server.h"
22 #include "workspace.h"
23 #include "xcursortheme.h"
24 #include <kwineffects.h>
25 
26 #include <KWayland/Client/buffer.h>
27 #include <KWayland/Client/connection_thread.h>
28 #include <KWayland/Client/compositor.h>
29 #include <KWayland/Client/pointer.h>
30 #include <KWayland/Client/region.h>
31 #include <KWayland/Client/seat.h>
32 #include <KWayland/Client/server_decoration.h>
33 #include <KWayland/Client/shm_pool.h>
34 #include <KWayland/Client/surface.h>
35 
36 #include <KWaylandServer/clientconnection.h>
37 #include <KWaylandServer/seat_interface.h>
38 
39 #include <linux/input.h>
40 
41 namespace KWin
42 {
43 
loadReferenceThemeCursor_helper(const KXcursorTheme & theme,const QByteArray & name)44 static PlatformCursorImage loadReferenceThemeCursor_helper(const KXcursorTheme &theme,
45                                                            const QByteArray &name)
46 {
47     const QVector<KXcursorSprite> sprites = theme.shape(name);
48     if (sprites.isEmpty()) {
49         return PlatformCursorImage();
50     }
51 
52     return PlatformCursorImage(sprites.constFirst().data(), sprites.constFirst().hotspot());
53 }
54 
loadReferenceThemeCursor(const QByteArray & name)55 static PlatformCursorImage loadReferenceThemeCursor(const QByteArray &name)
56 {
57     const Cursor *pointerCursor = Cursors::self()->mouse();
58 
59     const KXcursorTheme theme = KXcursorTheme::fromTheme(pointerCursor->themeName(),
60                                                          pointerCursor->themeSize(),
61                                                          screens()->maxScale());
62     if (theme.isEmpty()) {
63         return PlatformCursorImage();
64     }
65 
66     PlatformCursorImage platformCursorImage = loadReferenceThemeCursor_helper(theme, name);
67     if (!platformCursorImage.isNull()) {
68         return platformCursorImage;
69     }
70 
71     const QVector<QByteArray> alternativeNames = Cursor::cursorAlternativeNames(name);
72     for (const QByteArray &alternativeName : alternativeNames) {
73         platformCursorImage = loadReferenceThemeCursor_helper(theme, alternativeName);
74         if (!platformCursorImage.isNull()) {
75             break;
76         }
77     }
78 
79     return platformCursorImage;
80 }
81 
loadReferenceThemeCursor(const CursorShape & shape)82 static PlatformCursorImage loadReferenceThemeCursor(const CursorShape &shape)
83 {
84     return loadReferenceThemeCursor(shape.name());
85 }
86 
87 static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_input-0");
88 
89 class PointerInputTest : public QObject
90 {
91     Q_OBJECT
92 private Q_SLOTS:
93     void initTestCase();
94     void init();
95     void cleanup();
96     void testWarpingUpdatesFocus();
97     void testWarpingGeneratesPointerMotion();
98     void testWarpingDuringFilter();
99     void testUpdateFocusAfterScreenChange();
100     void testModifierClickUnrestrictedMove_data();
101     void testModifierClickUnrestrictedMove();
102     void testModifierClickUnrestrictedMoveGlobalShortcutsDisabled();
103     void testModifierScrollOpacity_data();
104     void testModifierScrollOpacity();
105     void testModifierScrollOpacityGlobalShortcutsDisabled();
106     void testScrollAction();
107     void testFocusFollowsMouse();
108     void testMouseActionInactiveWindow_data();
109     void testMouseActionInactiveWindow();
110     void testMouseActionActiveWindow_data();
111     void testMouseActionActiveWindow();
112     void testCursorImage();
113     void testEffectOverrideCursorImage();
114     void testPopup();
115     void testDecoCancelsPopup();
116     void testWindowUnderCursorWhileButtonPressed();
117     void testConfineToScreenGeometry_data();
118     void testConfineToScreenGeometry();
119     void testResizeCursor_data();
120     void testResizeCursor();
121     void testMoveCursor();
122     void testHideShowCursor();
123     void testDefaultInputRegion();
124     void testEmptyInputRegion();
125 
126 private:
127     void render(KWayland::Client::Surface *surface, const QSize &size = QSize(100, 50));
128     KWayland::Client::Compositor *m_compositor = nullptr;
129     KWayland::Client::Seat *m_seat = nullptr;
130 };
131 
initTestCase()132 void PointerInputTest::initTestCase()
133 {
134     qRegisterMetaType<KWin::AbstractClient*>();
135     qRegisterMetaType<KWin::Deleted*>();
136     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
137     QVERIFY(applicationStartedSpy.isValid());
138     kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
139     QVERIFY(waylandServer()->init(s_socketName));
140     QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2));
141 
142     kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));
143 
144     if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) {
145         qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White"));
146     } else {
147         // might be vanilla-dmz (e.g. Arch, FreeBSD)
148         qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ"));
149     }
150     qputenv("XCURSOR_SIZE", QByteArrayLiteral("24"));
151     qputenv("XKB_DEFAULT_RULES", "evdev");
152 
153     kwinApp()->start();
154     QVERIFY(applicationStartedSpy.wait());
155     const auto outputs = kwinApp()->platform()->enabledOutputs();
156     QCOMPARE(outputs.count(), 2);
157     QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
158     QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
159     setenv("QT_QPA_PLATFORM", "wayland", true);
160     Test::initWaylandWorkspace();
161 }
162 
init()163 void PointerInputTest::init()
164 {
165     QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration));
166     QVERIFY(Test::waitForWaylandPointer());
167     m_compositor = Test::waylandCompositor();
168     m_seat = Test::waylandSeat();
169 
170     workspace()->setActiveOutput(QPoint(640, 512));
171     Cursors::self()->mouse()->setPos(QPoint(640, 512));
172 }
173 
cleanup()174 void PointerInputTest::cleanup()
175 {
176     Test::destroyWaylandConnection();
177 }
178 
render(KWayland::Client::Surface * surface,const QSize & size)179 void PointerInputTest::render(KWayland::Client::Surface *surface, const QSize &size)
180 {
181     Test::render(surface, size, Qt::blue);
182     Test::flushWaylandConnection();
183 }
184 
testWarpingUpdatesFocus()185 void PointerInputTest::testWarpingUpdatesFocus()
186 {
187     // this test verifies that warping the pointer creates pointer enter and leave events
188     using namespace KWayland::Client;
189     // create pointer and signal spy for enter and leave signals
190     auto pointer = m_seat->createPointer(m_seat);
191     QVERIFY(pointer);
192     QVERIFY(pointer->isValid());
193     QSignalSpy enteredSpy(pointer, &Pointer::entered);
194     QVERIFY(enteredSpy.isValid());
195     QSignalSpy leftSpy(pointer, &Pointer::left);
196     QVERIFY(leftSpy.isValid());
197 
198     // create a window
199     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
200     QVERIFY(clientAddedSpy.isValid());
201     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
202     QVERIFY(surface);
203     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
204     QVERIFY(shellSurface);
205     render(surface);
206     QVERIFY(clientAddedSpy.wait());
207     AbstractClient *window = workspace()->activeClient();
208     QVERIFY(window);
209 
210     // currently there should not be a focused pointer surface
211     QVERIFY(!waylandServer()->seat()->focusedPointerSurface());
212     QVERIFY(!pointer->enteredSurface());
213 
214     // enter
215     Cursors::self()->mouse()->setPos(QPoint(25, 25));
216     QVERIFY(enteredSpy.wait());
217     QCOMPARE(enteredSpy.count(), 1);
218     QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25));
219     // window should have focus
220     QCOMPARE(pointer->enteredSurface(), surface);
221     // also on the server
222     QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface());
223 
224     // and out again
225     Cursors::self()->mouse()->setPos(QPoint(250, 250));;
226     QVERIFY(leftSpy.wait());
227     QCOMPARE(leftSpy.count(), 1);
228     // there should not be a focused pointer surface anymore
229     QVERIFY(!waylandServer()->seat()->focusedPointerSurface());
230     QVERIFY(!pointer->enteredSurface());
231 }
232 
testWarpingGeneratesPointerMotion()233 void PointerInputTest::testWarpingGeneratesPointerMotion()
234 {
235     // this test verifies that warping the pointer creates pointer motion events
236     using namespace KWayland::Client;
237     // create pointer and signal spy for enter and motion
238     auto pointer = m_seat->createPointer(m_seat);
239     QVERIFY(pointer);
240     QVERIFY(pointer->isValid());
241     QSignalSpy enteredSpy(pointer, &Pointer::entered);
242     QVERIFY(enteredSpy.isValid());
243     QSignalSpy movedSpy(pointer, &Pointer::motion);
244     QVERIFY(movedSpy.isValid());
245 
246     // create a window
247     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
248     QVERIFY(clientAddedSpy.isValid());
249     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
250     QVERIFY(surface);
251     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
252     QVERIFY(shellSurface);
253     render(surface);
254     QVERIFY(clientAddedSpy.wait());
255     AbstractClient *window = workspace()->activeClient();
256     QVERIFY(window);
257 
258     // enter
259     kwinApp()->platform()->pointerMotion(QPointF(25, 25), 1);
260     QVERIFY(enteredSpy.wait());
261     QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25));
262 
263     // now warp
264     Cursors::self()->mouse()->setPos(QPoint(26, 26));
265     QVERIFY(movedSpy.wait());
266     QCOMPARE(movedSpy.count(), 1);
267     QCOMPARE(movedSpy.last().first().toPointF(), QPointF(26, 26));
268 }
269 
testWarpingDuringFilter()270 void PointerInputTest::testWarpingDuringFilter()
271 {
272     // this test verifies that pointer motion is handled correctly if
273     // the pointer gets warped during processing of input events
274     using namespace KWayland::Client;
275 
276     // create pointer
277     auto pointer = m_seat->createPointer(m_seat);
278     QVERIFY(pointer);
279     QVERIFY(pointer->isValid());
280     QSignalSpy movedSpy(pointer, &Pointer::motion);
281     QVERIFY(movedSpy.isValid());
282 
283     // warp cursor into expected geometry
284     Cursors::self()->mouse()->setPos(10, 10);
285 
286     // create a window
287     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
288     QVERIFY(clientAddedSpy.isValid());
289     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
290     QVERIFY(surface);
291     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
292     QVERIFY(shellSurface);
293     render(surface);
294     QVERIFY(clientAddedSpy.wait());
295     AbstractClient *window = workspace()->activeClient();
296     QVERIFY(window);
297 
298     QCOMPARE(window->pos(), QPoint(0, 0));
299     QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos()));
300 
301     // is PresentWindows effect for top left screen edge loaded
302     QVERIFY(static_cast<EffectsHandlerImpl*>(effects)->isEffectLoaded("presentwindows"));
303     QVERIFY(movedSpy.isEmpty());
304     quint32 timestamp = 0;
305     kwinApp()->platform()->pointerMotion(QPoint(0, 0), timestamp++);
306     // screen edges push back
307     QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(1, 1));
308     QVERIFY(movedSpy.wait());
309     QCOMPARE(movedSpy.count(), 2);
310     QCOMPARE(movedSpy.at(0).first().toPoint(), QPoint(0, 0));
311     QCOMPARE(movedSpy.at(1).first().toPoint(), QPoint(1, 1));
312 }
313 
testUpdateFocusAfterScreenChange()314 void PointerInputTest::testUpdateFocusAfterScreenChange()
315 {
316     // this test verifies that a pointer enter event is generated when the cursor changes to another
317     // screen due to removal of screen
318     using namespace KWayland::Client;
319 
320     // create pointer and signal spy for enter and motion
321     auto pointer = m_seat->createPointer(m_seat);
322     QVERIFY(pointer);
323     QVERIFY(pointer->isValid());
324     QSignalSpy enteredSpy(pointer, &Pointer::entered);
325     QVERIFY(enteredSpy.isValid());
326     QSignalSpy leftSpy(pointer, &Pointer::left);
327     QVERIFY(leftSpy.isValid());
328 
329     // create a window
330     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
331     QVERIFY(clientAddedSpy.isValid());
332     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
333     QVERIFY(surface);
334     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
335     QVERIFY(shellSurface);
336     render(surface, QSize(1280, 1024));
337     QVERIFY(clientAddedSpy.wait());
338     AbstractClient *window = workspace()->activeClient();
339     QVERIFY(window);
340     QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos()));
341     QVERIFY(enteredSpy.wait());
342     QCOMPARE(enteredSpy.count(), 1);
343 
344     // move the cursor to the second screen
345     Cursors::self()->mouse()->setPos(1500, 300);
346     QVERIFY(!window->frameGeometry().contains(Cursors::self()->mouse()->pos()));
347     QVERIFY(leftSpy.wait());
348 
349     QSignalSpy screensChangedSpy(screens(), &Screens::changed);
350     QVERIFY(screensChangedSpy.isValid());
351     // now let's remove the screen containing the cursor
352     QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs",
353                               Qt::DirectConnection,
354                               Q_ARG(int, 1),
355                               Q_ARG(QVector<QRect>, QVector<QRect>{QRect(0, 0, 1280, 1024)}));
356     QVERIFY(screensChangedSpy.wait());
357     QCOMPARE(screens()->count(), 1);
358 
359     // this should have warped the cursor
360     QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(639, 511));
361     QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos()));
362 
363     // and we should get an enter event
364     QVERIFY(enteredSpy.wait());
365     QCOMPARE(enteredSpy.count(), 2);
366 }
367 
testModifierClickUnrestrictedMove_data()368 void PointerInputTest::testModifierClickUnrestrictedMove_data()
369 {
370     QTest::addColumn<int>("modifierKey");
371     QTest::addColumn<int>("mouseButton");
372     QTest::addColumn<QString>("modKey");
373     QTest::addColumn<bool>("capsLock");
374 
375     const QString alt = QStringLiteral("Alt");
376     const QString meta = QStringLiteral("Meta");
377 
378     QTest::newRow("Left Alt + Left Click")    << KEY_LEFTALT  << BTN_LEFT   << alt << false;
379     QTest::newRow("Left Alt + Right Click")   << KEY_LEFTALT  << BTN_RIGHT  << alt << false;
380     QTest::newRow("Left Alt + Middle Click")  << KEY_LEFTALT  << BTN_MIDDLE << alt << false;
381     QTest::newRow("Right Alt + Left Click")   << KEY_RIGHTALT << BTN_LEFT   << alt << false;
382     QTest::newRow("Right Alt + Right Click")  << KEY_RIGHTALT << BTN_RIGHT  << alt << false;
383     QTest::newRow("Right Alt + Middle Click") << KEY_RIGHTALT << BTN_MIDDLE << alt << false;
384     // now everything with meta
385     QTest::newRow("Left Meta + Left Click")    << KEY_LEFTMETA  << BTN_LEFT   << meta << false;
386     QTest::newRow("Left Meta + Right Click")   << KEY_LEFTMETA  << BTN_RIGHT  << meta << false;
387     QTest::newRow("Left Meta + Middle Click")  << KEY_LEFTMETA  << BTN_MIDDLE << meta << false;
388     QTest::newRow("Right Meta + Left Click")   << KEY_RIGHTMETA << BTN_LEFT   << meta << false;
389     QTest::newRow("Right Meta + Right Click")  << KEY_RIGHTMETA << BTN_RIGHT  << meta << false;
390     QTest::newRow("Right Meta + Middle Click") << KEY_RIGHTMETA << BTN_MIDDLE << meta << false;
391 
392     // and with capslock
393     QTest::newRow("Left Alt + Left Click/CapsLock")    << KEY_LEFTALT  << BTN_LEFT   << alt << true;
394     QTest::newRow("Left Alt + Right Click/CapsLock")   << KEY_LEFTALT  << BTN_RIGHT  << alt << true;
395     QTest::newRow("Left Alt + Middle Click/CapsLock")  << KEY_LEFTALT  << BTN_MIDDLE << alt << true;
396     QTest::newRow("Right Alt + Left Click/CapsLock")   << KEY_RIGHTALT << BTN_LEFT   << alt << true;
397     QTest::newRow("Right Alt + Right Click/CapsLock")  << KEY_RIGHTALT << BTN_RIGHT  << alt << true;
398     QTest::newRow("Right Alt + Middle Click/CapsLock") << KEY_RIGHTALT << BTN_MIDDLE << alt << true;
399     // now everything with meta
400     QTest::newRow("Left Meta + Left Click/CapsLock")    << KEY_LEFTMETA  << BTN_LEFT   << meta << true;
401     QTest::newRow("Left Meta + Right Click/CapsLock")   << KEY_LEFTMETA  << BTN_RIGHT  << meta << true;
402     QTest::newRow("Left Meta + Middle Click/CapsLock")  << KEY_LEFTMETA  << BTN_MIDDLE << meta << true;
403     QTest::newRow("Right Meta + Left Click/CapsLock")   << KEY_RIGHTMETA << BTN_LEFT   << meta << true;
404     QTest::newRow("Right Meta + Right Click/CapsLock")  << KEY_RIGHTMETA << BTN_RIGHT  << meta << true;
405     QTest::newRow("Right Meta + Middle Click/CapsLock") << KEY_RIGHTMETA << BTN_MIDDLE << meta << true;
406 }
407 
testModifierClickUnrestrictedMove()408 void PointerInputTest::testModifierClickUnrestrictedMove()
409 {
410     // this test ensures that Alt+mouse button press triggers unrestricted move
411     using namespace KWayland::Client;
412     // create pointer and signal spy for button events
413     auto pointer = m_seat->createPointer(m_seat);
414     QVERIFY(pointer);
415     QVERIFY(pointer->isValid());
416     QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged);
417     QVERIFY(buttonSpy.isValid());
418 
419     // first modify the config for this run
420     QFETCH(QString, modKey);
421     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
422     group.writeEntry("CommandAllKey", modKey);
423     group.writeEntry("CommandAll1", "Move");
424     group.writeEntry("CommandAll2", "Move");
425     group.writeEntry("CommandAll3", "Move");
426     group.sync();
427     workspace()->slotReconfigure();
428     QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier);
429     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
430     QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove);
431     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove);
432 
433     // create a window
434     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
435     QVERIFY(clientAddedSpy.isValid());
436     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
437     QVERIFY(surface);
438     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
439     QVERIFY(shellSurface);
440     render(surface);
441     QVERIFY(clientAddedSpy.wait());
442     AbstractClient *window = workspace()->activeClient();
443     QVERIFY(window);
444 
445     // move cursor on window
446     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
447 
448     // simulate modifier+click
449     quint32 timestamp = 1;
450     QFETCH(bool, capsLock);
451     if (capsLock) {
452         kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++);
453     }
454     QFETCH(int, modifierKey);
455     QFETCH(int, mouseButton);
456     kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++);
457     QVERIFY(!window->isInteractiveMove());
458     kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++);
459     QVERIFY(window->isInteractiveMove());
460     // release modifier should not change it
461     kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++);
462     QVERIFY(window->isInteractiveMove());
463     // but releasing the key should end move/resize
464     kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++);
465     QVERIFY(!window->isInteractiveMove());
466     if (capsLock) {
467         kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++);
468     }
469 
470     // all of that should not have triggered button events on the surface
471     QCOMPARE(buttonSpy.count(), 0);
472     // also waiting shouldn't give us the event
473     QVERIFY(!buttonSpy.wait(100));
474 }
475 
testModifierClickUnrestrictedMoveGlobalShortcutsDisabled()476 void PointerInputTest::testModifierClickUnrestrictedMoveGlobalShortcutsDisabled()
477 {
478     // this test ensures that Alt+mouse button press triggers unrestricted move
479     using namespace KWayland::Client;
480     // create pointer and signal spy for button events
481     auto pointer = m_seat->createPointer(m_seat);
482     QVERIFY(pointer);
483     QVERIFY(pointer->isValid());
484     QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged);
485     QVERIFY(buttonSpy.isValid());
486 
487     // first modify the config for this run
488     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
489     group.writeEntry("CommandAllKey", "Meta");
490     group.writeEntry("CommandAll1", "Move");
491     group.writeEntry("CommandAll2", "Move");
492     group.writeEntry("CommandAll3", "Move");
493     group.sync();
494     workspace()->slotReconfigure();
495     QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
496     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
497     QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove);
498     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove);
499 
500     // create a window
501     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
502     QVERIFY(clientAddedSpy.isValid());
503     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
504     QVERIFY(surface);
505     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
506     QVERIFY(shellSurface);
507     render(surface);
508     QVERIFY(clientAddedSpy.wait());
509     AbstractClient *window = workspace()->activeClient();
510     QVERIFY(window);
511 
512     // disable global shortcuts
513     QVERIFY(!workspace()->globalShortcutsDisabled());
514     workspace()->disableGlobalShortcutsForClient(true);
515     QVERIFY(workspace()->globalShortcutsDisabled());
516 
517     // move cursor on window
518     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
519 
520     // simulate modifier+click
521     quint32 timestamp = 1;
522     kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++);
523     QVERIFY(!window->isInteractiveMove());
524     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
525     QVERIFY(!window->isInteractiveMove());
526     // release modifier should not change it
527     kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++);
528     QVERIFY(!window->isInteractiveMove());
529     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
530 
531     workspace()->disableGlobalShortcutsForClient(false);
532 }
533 
testModifierScrollOpacity_data()534 void PointerInputTest::testModifierScrollOpacity_data()
535 {
536     QTest::addColumn<int>("modifierKey");
537     QTest::addColumn<QString>("modKey");
538     QTest::addColumn<bool>("capsLock");
539 
540     const QString alt = QStringLiteral("Alt");
541     const QString meta = QStringLiteral("Meta");
542 
543     QTest::newRow("Left Alt")   << KEY_LEFTALT  << alt << false;
544     QTest::newRow("Right Alt")  << KEY_RIGHTALT << alt << false;
545     QTest::newRow("Left Meta")  << KEY_LEFTMETA  << meta << false;
546     QTest::newRow("Right Meta") << KEY_RIGHTMETA << meta << false;
547     QTest::newRow("Left Alt/CapsLock")   << KEY_LEFTALT  << alt << true;
548     QTest::newRow("Right Alt/CapsLock")  << KEY_RIGHTALT << alt << true;
549     QTest::newRow("Left Meta/CapsLock")  << KEY_LEFTMETA  << meta << true;
550     QTest::newRow("Right Meta/CapsLock") << KEY_RIGHTMETA << meta << true;
551 }
552 
testModifierScrollOpacity()553 void PointerInputTest::testModifierScrollOpacity()
554 {
555     // this test verifies that mod+wheel performs a window operation and does not
556     // pass the wheel to the window
557     using namespace KWayland::Client;
558     // create pointer and signal spy for button events
559     auto pointer = m_seat->createPointer(m_seat);
560     QVERIFY(pointer);
561     QVERIFY(pointer->isValid());
562     QSignalSpy axisSpy(pointer, &Pointer::axisChanged);
563     QVERIFY(axisSpy.isValid());
564 
565     // first modify the config for this run
566     QFETCH(QString, modKey);
567     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
568     group.writeEntry("CommandAllKey", modKey);
569     group.writeEntry("CommandAllWheel", "change opacity");
570     group.sync();
571     workspace()->slotReconfigure();
572 
573     // create a window
574     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
575     QVERIFY(clientAddedSpy.isValid());
576     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
577     QVERIFY(surface);
578     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
579     QVERIFY(shellSurface);
580     render(surface);
581     QVERIFY(clientAddedSpy.wait());
582     AbstractClient *window = workspace()->activeClient();
583     QVERIFY(window);
584     // set the opacity to 0.5
585     window->setOpacity(0.5);
586     QCOMPARE(window->opacity(), 0.5);
587 
588     // move cursor on window
589     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
590 
591     // simulate modifier+wheel
592     quint32 timestamp = 1;
593     QFETCH(bool, capsLock);
594     if (capsLock) {
595         kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++);
596     }
597     QFETCH(int, modifierKey);
598     kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++);
599     kwinApp()->platform()->pointerAxisVertical(-5, timestamp++);
600     QCOMPARE(window->opacity(), 0.6);
601     kwinApp()->platform()->pointerAxisVertical(5, timestamp++);
602     QCOMPARE(window->opacity(), 0.5);
603     kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++);
604     if (capsLock) {
605         kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++);
606     }
607 
608     // axis should have been filtered out
609     QCOMPARE(axisSpy.count(), 0);
610     QVERIFY(!axisSpy.wait(100));
611 }
612 
testModifierScrollOpacityGlobalShortcutsDisabled()613 void PointerInputTest::testModifierScrollOpacityGlobalShortcutsDisabled()
614 {
615     // this test verifies that mod+wheel performs a window operation and does not
616     // pass the wheel to the window
617     using namespace KWayland::Client;
618     // create pointer and signal spy for button events
619     auto pointer = m_seat->createPointer(m_seat);
620     QVERIFY(pointer);
621     QVERIFY(pointer->isValid());
622     QSignalSpy axisSpy(pointer, &Pointer::axisChanged);
623     QVERIFY(axisSpy.isValid());
624 
625     // first modify the config for this run
626     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
627     group.writeEntry("CommandAllKey", "Meta");
628     group.writeEntry("CommandAllWheel", "change opacity");
629     group.sync();
630     workspace()->slotReconfigure();
631 
632     // create a window
633     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
634     QVERIFY(clientAddedSpy.isValid());
635     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
636     QVERIFY(surface);
637     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
638     QVERIFY(shellSurface);
639     render(surface);
640     QVERIFY(clientAddedSpy.wait());
641     AbstractClient *window = workspace()->activeClient();
642     QVERIFY(window);
643     // set the opacity to 0.5
644     window->setOpacity(0.5);
645     QCOMPARE(window->opacity(), 0.5);
646 
647     // move cursor on window
648     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
649 
650     // disable global shortcuts
651     QVERIFY(!workspace()->globalShortcutsDisabled());
652     workspace()->disableGlobalShortcutsForClient(true);
653     QVERIFY(workspace()->globalShortcutsDisabled());
654 
655     // simulate modifier+wheel
656     quint32 timestamp = 1;
657     kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++);
658     kwinApp()->platform()->pointerAxisVertical(-5, timestamp++);
659     QCOMPARE(window->opacity(), 0.5);
660     kwinApp()->platform()->pointerAxisVertical(5, timestamp++);
661     QCOMPARE(window->opacity(), 0.5);
662     kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++);
663 
664     workspace()->disableGlobalShortcutsForClient(false);
665 }
666 
testScrollAction()667 void  PointerInputTest::testScrollAction()
668 {
669     // this test verifies that scroll on inactive window performs a mouse action
670     using namespace KWayland::Client;
671     auto pointer = m_seat->createPointer(m_seat);
672     QVERIFY(pointer);
673     QVERIFY(pointer->isValid());
674     QSignalSpy axisSpy(pointer, &Pointer::axisChanged);
675     QVERIFY(axisSpy.isValid());
676 
677     // first modify the config for this run
678     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
679     group.writeEntry("CommandWindowWheel", "activate and scroll");
680     group.sync();
681     workspace()->slotReconfigure();
682     // create two windows
683     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
684     QVERIFY(clientAddedSpy.isValid());
685     KWayland::Client::Surface *surface1 = Test::createSurface(m_compositor);
686     QVERIFY(surface1);
687     Test::XdgToplevel *shellSurface1 = Test::createXdgToplevelSurface(surface1, surface1);
688     QVERIFY(shellSurface1);
689     render(surface1);
690     QVERIFY(clientAddedSpy.wait());
691     AbstractClient *window1 = workspace()->activeClient();
692     QVERIFY(window1);
693     KWayland::Client::Surface *surface2 = Test::createSurface(m_compositor);
694     QVERIFY(surface2);
695     Test::XdgToplevel *shellSurface2 = Test::createXdgToplevelSurface(surface2, surface2);
696     QVERIFY(shellSurface2);
697     render(surface2);
698     QVERIFY(clientAddedSpy.wait());
699     AbstractClient *window2 = workspace()->activeClient();
700     QVERIFY(window2);
701     QVERIFY(window1 != window2);
702 
703     // move cursor to the inactive window
704     Cursors::self()->mouse()->setPos(window1->frameGeometry().center());
705 
706     quint32 timestamp = 1;
707     QVERIFY(!window1->isActive());
708     kwinApp()->platform()->pointerAxisVertical(5, timestamp++);
709     QVERIFY(window1->isActive());
710 
711     // but also the wheel event should be passed to the window
712     QVERIFY(axisSpy.wait());
713 
714     // we need to wait a little bit, otherwise the test crashes in effectshandler, needs fixing
715     QTest::qWait(100);
716 }
717 
testFocusFollowsMouse()718 void PointerInputTest::testFocusFollowsMouse()
719 {
720     using namespace KWayland::Client;
721     // need to create a pointer, otherwise it doesn't accept focus
722     auto pointer = m_seat->createPointer(m_seat);
723     QVERIFY(pointer);
724     QVERIFY(pointer->isValid());
725     // move cursor out of the way of first window to be created
726     Cursors::self()->mouse()->setPos(900, 900);
727 
728     // first modify the config for this run
729     KConfigGroup group = kwinApp()->config()->group("Windows");
730     group.writeEntry("AutoRaise", true);
731     group.writeEntry("AutoRaiseInterval", 20);
732     group.writeEntry("DelayFocusInterval", 200);
733     group.writeEntry("FocusPolicy", "FocusFollowsMouse");
734     group.sync();
735     workspace()->slotReconfigure();
736     // verify the settings
737     QCOMPARE(options->focusPolicy(), Options::FocusFollowsMouse);
738     QVERIFY(options->isAutoRaise());
739     QCOMPARE(options->autoRaiseInterval(), 20);
740     QCOMPARE(options->delayFocusInterval(), 200);
741 
742     // create two windows
743     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
744     QVERIFY(clientAddedSpy.isValid());
745     KWayland::Client::Surface *surface1 = Test::createSurface(m_compositor);
746     QVERIFY(surface1);
747     Test::XdgToplevel *shellSurface1 = Test::createXdgToplevelSurface(surface1, surface1);
748     QVERIFY(shellSurface1);
749     render(surface1, QSize(800, 800));
750     QVERIFY(clientAddedSpy.wait());
751     AbstractClient *window1 = workspace()->activeClient();
752     QVERIFY(window1);
753     KWayland::Client::Surface *surface2 = Test::createSurface(m_compositor);
754     QVERIFY(surface2);
755     Test::XdgToplevel *shellSurface2 = Test::createXdgToplevelSurface(surface2, surface2);
756     QVERIFY(shellSurface2);
757     render(surface2, QSize(800, 800));
758     QVERIFY(clientAddedSpy.wait());
759     AbstractClient *window2 = workspace()->activeClient();
760     QVERIFY(window2);
761     QVERIFY(window1 != window2);
762     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2);
763     // geometry of the two windows should be overlapping
764     QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry()));
765 
766     // signal spies for active window changed and stacking order changed
767     QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::clientActivated);
768     QVERIFY(activeWindowChangedSpy.isValid());
769     QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged);
770     QVERIFY(stackingOrderChangedSpy.isValid());
771 
772     QVERIFY(!window1->isActive());
773     QVERIFY(window2->isActive());
774 
775     // move on top of first window
776     QVERIFY(window1->frameGeometry().contains(10, 10));
777     QVERIFY(!window2->frameGeometry().contains(10, 10));
778     Cursors::self()->mouse()->setPos(10, 10);
779     QVERIFY(stackingOrderChangedSpy.wait());
780     QCOMPARE(stackingOrderChangedSpy.count(), 1);
781     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1);
782     QTRY_VERIFY(window1->isActive());
783 
784     // move on second window, but move away before active window change delay hits
785     Cursors::self()->mouse()->setPos(810, 810);
786     QVERIFY(stackingOrderChangedSpy.wait());
787     QCOMPARE(stackingOrderChangedSpy.count(), 2);
788     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2);
789     Cursors::self()->mouse()->setPos(10, 10);
790     QVERIFY(!activeWindowChangedSpy.wait(250));
791     QVERIFY(window1->isActive());
792     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1);
793     // as we moved back on window 1 that should been raised in the mean time
794     QCOMPARE(stackingOrderChangedSpy.count(), 3);
795 
796     // quickly move on window 2 and back on window 1 should not raise window 2
797     Cursors::self()->mouse()->setPos(810, 810);
798     Cursors::self()->mouse()->setPos(10, 10);
799     QVERIFY(!stackingOrderChangedSpy.wait(250));
800 }
801 
testMouseActionInactiveWindow_data()802 void PointerInputTest::testMouseActionInactiveWindow_data()
803 {
804     QTest::addColumn<quint32>("button");
805 
806     QTest::newRow("Left")   << quint32(BTN_LEFT);
807     QTest::newRow("Middle") << quint32(BTN_MIDDLE);
808     QTest::newRow("Right")  << quint32(BTN_RIGHT);
809 }
810 
testMouseActionInactiveWindow()811 void PointerInputTest::testMouseActionInactiveWindow()
812 {
813     // this test performs the mouse button window action on an inactive window
814     // it should activate the window and raise it
815     using namespace KWayland::Client;
816 
817     // first modify the config for this run - disable FocusFollowsMouse
818     KConfigGroup group = kwinApp()->config()->group("Windows");
819     group.writeEntry("FocusPolicy", "ClickToFocus");
820     group.sync();
821     group = kwinApp()->config()->group("MouseBindings");
822     group.writeEntry("CommandWindow1", "Activate, raise and pass click");
823     group.writeEntry("CommandWindow2", "Activate, raise and pass click");
824     group.writeEntry("CommandWindow3", "Activate, raise and pass click");
825     group.sync();
826     workspace()->slotReconfigure();
827 
828     // create two windows
829     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
830     QVERIFY(clientAddedSpy.isValid());
831     KWayland::Client::Surface *surface1 = Test::createSurface(m_compositor);
832     QVERIFY(surface1);
833     Test::XdgToplevel *shellSurface1 = Test::createXdgToplevelSurface(surface1, surface1);
834     QVERIFY(shellSurface1);
835     render(surface1, QSize(800, 800));
836     QVERIFY(clientAddedSpy.wait());
837     AbstractClient *window1 = workspace()->activeClient();
838     QVERIFY(window1);
839     KWayland::Client::Surface *surface2 = Test::createSurface(m_compositor);
840     QVERIFY(surface2);
841     Test::XdgToplevel *shellSurface2 = Test::createXdgToplevelSurface(surface2, surface2);
842     QVERIFY(shellSurface2);
843     render(surface2, QSize(800, 800));
844     QVERIFY(clientAddedSpy.wait());
845     AbstractClient *window2 = workspace()->activeClient();
846     QVERIFY(window2);
847     QVERIFY(window1 != window2);
848     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2);
849     // geometry of the two windows should be overlapping
850     QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry()));
851 
852     // signal spies for active window changed and stacking order changed
853     QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::clientActivated);
854     QVERIFY(activeWindowChangedSpy.isValid());
855     QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged);
856     QVERIFY(stackingOrderChangedSpy.isValid());
857 
858     QVERIFY(!window1->isActive());
859     QVERIFY(window2->isActive());
860 
861     // move on top of first window
862     QVERIFY(window1->frameGeometry().contains(10, 10));
863     QVERIFY(!window2->frameGeometry().contains(10, 10));
864     Cursors::self()->mouse()->setPos(10, 10);
865     // no focus follows mouse
866     QVERIFY(!stackingOrderChangedSpy.wait(200));
867     QVERIFY(stackingOrderChangedSpy.isEmpty());
868     QVERIFY(activeWindowChangedSpy.isEmpty());
869     QVERIFY(window2->isActive());
870     // and click
871     quint32 timestamp = 1;
872     QFETCH(quint32, button);
873     kwinApp()->platform()->pointerButtonPressed(button, timestamp++);
874     // should raise window1 and activate it
875     QCOMPARE(stackingOrderChangedSpy.count(), 1);
876     QVERIFY(!activeWindowChangedSpy.isEmpty());
877     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1);
878     QVERIFY(window1->isActive());
879     QVERIFY(!window2->isActive());
880 
881     // release again
882     kwinApp()->platform()->pointerButtonReleased(button, timestamp++);
883 }
884 
testMouseActionActiveWindow_data()885 void PointerInputTest::testMouseActionActiveWindow_data()
886 {
887     QTest::addColumn<bool>("clickRaise");
888     QTest::addColumn<quint32>("button");
889 
890     for (quint32 i=BTN_LEFT; i < BTN_JOYSTICK; i++) {
891         QByteArray number = QByteArray::number(i, 16);
892         QTest::newRow(QByteArrayLiteral("click raise/").append(number).constData()) << true << i;
893         QTest::newRow(QByteArrayLiteral("no click raise/").append(number).constData()) << false << i;
894     }
895 }
896 
testMouseActionActiveWindow()897 void PointerInputTest::testMouseActionActiveWindow()
898 {
899     // this test verifies the mouse action performed on an active window
900     // for all buttons it should trigger a window raise depending on the
901     // click raise option
902     using namespace KWayland::Client;
903     // create a button spy - all clicks should be passed through
904     auto pointer = m_seat->createPointer(m_seat);
905     QVERIFY(pointer);
906     QVERIFY(pointer->isValid());
907     QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged);
908     QVERIFY(buttonSpy.isValid());
909 
910     // adjust config for this run
911     QFETCH(bool, clickRaise);
912     KConfigGroup group = kwinApp()->config()->group("Windows");
913     group.writeEntry("ClickRaise", clickRaise);
914     group.sync();
915     workspace()->slotReconfigure();
916     QCOMPARE(options->isClickRaise(), clickRaise);
917 
918     // create two windows
919     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
920     QVERIFY(clientAddedSpy.isValid());
921     KWayland::Client::Surface *surface1 = Test::createSurface(m_compositor);
922     QVERIFY(surface1);
923     Test::XdgToplevel *shellSurface1 = Test::createXdgToplevelSurface(surface1, surface1);
924     QVERIFY(shellSurface1);
925     render(surface1, QSize(800, 800));
926     QVERIFY(clientAddedSpy.wait());
927     AbstractClient *window1 = workspace()->activeClient();
928     QVERIFY(window1);
929     QSignalSpy window1DestroyedSpy(window1, &QObject::destroyed);
930     QVERIFY(window1DestroyedSpy.isValid());
931     KWayland::Client::Surface *surface2 = Test::createSurface(m_compositor);
932     QVERIFY(surface2);
933     Test::XdgToplevel *shellSurface2 = Test::createXdgToplevelSurface(surface2, surface2);
934     QVERIFY(shellSurface2);
935     render(surface2, QSize(800, 800));
936     QVERIFY(clientAddedSpy.wait());
937     AbstractClient *window2 = workspace()->activeClient();
938     QVERIFY(window2);
939     QVERIFY(window1 != window2);
940     QSignalSpy window2DestroyedSpy(window2, &QObject::destroyed);
941     QVERIFY(window2DestroyedSpy.isValid());
942     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2);
943     // geometry of the two windows should be overlapping
944     QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry()));
945     // lower the currently active window
946     workspace()->lowerClient(window2);
947     QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1);
948 
949     // signal spy for stacking order spy
950     QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged);
951     QVERIFY(stackingOrderChangedSpy.isValid());
952 
953     // move on top of second window
954     QVERIFY(!window1->frameGeometry().contains(900, 900));
955     QVERIFY(window2->frameGeometry().contains(900, 900));
956     Cursors::self()->mouse()->setPos(900, 900);
957 
958     // and click
959     quint32 timestamp = 1;
960     QFETCH(quint32, button);
961     kwinApp()->platform()->pointerButtonPressed(button, timestamp++);
962     QVERIFY(buttonSpy.wait());
963     if (clickRaise) {
964         QCOMPARE(stackingOrderChangedSpy.count(), 1);
965         QTRY_COMPARE_WITH_TIMEOUT(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2, 200);
966     } else {
967         QCOMPARE(stackingOrderChangedSpy.count(), 0);
968         QVERIFY(!stackingOrderChangedSpy.wait(100));
969         QCOMPARE(workspace()->topClientOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1);
970     }
971 
972     // release again
973     kwinApp()->platform()->pointerButtonReleased(button, timestamp++);
974 
975     delete surface1;
976     QVERIFY(window1DestroyedSpy.wait());
977     delete surface2;
978     QVERIFY(window2DestroyedSpy.wait());
979 }
980 
testCursorImage()981 void PointerInputTest::testCursorImage()
982 {
983     // this test verifies that the pointer image gets updated correctly from the client provided data
984     using namespace KWayland::Client;
985     // we need a pointer to get the enter event
986     auto pointer = m_seat->createPointer(m_seat);
987     QVERIFY(pointer);
988     QVERIFY(pointer->isValid());
989     QSignalSpy enteredSpy(pointer, &Pointer::entered);
990     QVERIFY(enteredSpy.isValid());
991 
992     // move cursor somewhere the new window won't open
993     auto cursor = Cursors::self()->mouse();
994     cursor->setPos(800, 800);
995     auto p = input()->pointer();
996     // at the moment it should be the fallback cursor
997     const QImage fallbackCursor = cursor->image();
998     QVERIFY(!fallbackCursor.isNull());
999 
1000     // create a window
1001     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
1002     QVERIFY(clientAddedSpy.isValid());
1003     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
1004     QVERIFY(surface);
1005     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
1006     QVERIFY(shellSurface);
1007     render(surface);
1008     QVERIFY(clientAddedSpy.wait());
1009     AbstractClient *window = workspace()->activeClient();
1010     QVERIFY(window);
1011 
1012     // move cursor to center of window, this should first set a null pointer, so we still show old cursor
1013     cursor->setPos(window->frameGeometry().center());
1014     QCOMPARE(p->focus(), window);
1015     QCOMPARE(cursor->image(), fallbackCursor);
1016     QVERIFY(enteredSpy.wait());
1017 
1018     // create a cursor on the pointer
1019     KWayland::Client::Surface *cursorSurface = Test::createSurface(m_compositor);
1020     QVERIFY(cursorSurface);
1021     QSignalSpy cursorRenderedSpy(cursorSurface, &KWayland::Client::Surface::frameRendered);
1022     QVERIFY(cursorRenderedSpy.isValid());
1023     QImage red = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied);
1024     red.fill(Qt::red);
1025     cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(red));
1026     cursorSurface->damage(QRect(0, 0, 10, 10));
1027     cursorSurface->commit();
1028     pointer->setCursor(cursorSurface, QPoint(5, 5));
1029     QVERIFY(cursorRenderedSpy.wait());
1030     QCOMPARE(cursor->image(), red);
1031     QCOMPARE(cursor->hotspot(), QPoint(5, 5));
1032     // change hotspot
1033     pointer->setCursor(cursorSurface, QPoint(6, 6));
1034     Test::flushWaylandConnection();
1035     QTRY_COMPARE(cursor->hotspot(), QPoint(6, 6));
1036     QCOMPARE(cursor->image(), red);
1037 
1038     // change the buffer
1039     QImage blue = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied);
1040     blue.fill(Qt::blue);
1041     auto b = Test::waylandShmPool()->createBuffer(blue);
1042     cursorSurface->attachBuffer(b);
1043     cursorSurface->damage(QRect(0, 0, 10, 10));
1044     cursorSurface->commit();
1045     QVERIFY(cursorRenderedSpy.wait());
1046     QTRY_COMPARE(cursor->image(), blue);
1047     QCOMPARE(cursor->hotspot(), QPoint(6, 6));
1048 
1049     // scaled cursor
1050     QImage blueScaled = QImage(QSize(20, 20), QImage::Format_ARGB32_Premultiplied);
1051     blueScaled.setDevicePixelRatio(2);
1052     blueScaled.fill(Qt::blue);
1053     auto bs = Test::waylandShmPool()->createBuffer(blueScaled);
1054     cursorSurface->attachBuffer(bs);
1055     cursorSurface->setScale(2);
1056     cursorSurface->damage(QRect(0, 0, 20, 20));
1057     cursorSurface->commit();
1058     QVERIFY(cursorRenderedSpy.wait());
1059     QTRY_COMPARE(cursor->image(), blueScaled);
1060     QCOMPARE(cursor->hotspot(), QPoint(6, 6)); //surface-local (so not changed)
1061 
1062     // hide the cursor
1063     pointer->setCursor(nullptr);
1064     Test::flushWaylandConnection();
1065     QTRY_VERIFY(cursor->image().isNull());
1066 
1067     // move cursor somewhere else, should reset to fallback cursor
1068     Cursors::self()->mouse()->setPos(window->frameGeometry().bottomLeft() + QPoint(20, 20));
1069     QVERIFY(!p->focus());
1070     QVERIFY(!cursor->image().isNull());
1071     QCOMPARE(cursor->image(), fallbackCursor);
1072 }
1073 
1074 class HelperEffect : public Effect
1075 {
1076     Q_OBJECT
1077 public:
HelperEffect()1078     HelperEffect() {}
~HelperEffect()1079     ~HelperEffect() override {}
1080 };
1081 
testEffectOverrideCursorImage()1082 void PointerInputTest::testEffectOverrideCursorImage()
1083 {
1084     // this test verifies the effect cursor override handling
1085     using namespace KWayland::Client;
1086     // we need a pointer to get the enter event and set a cursor
1087     auto pointer = m_seat->createPointer(m_seat);
1088     auto cursor = Cursors::self()->mouse();
1089     QVERIFY(pointer);
1090     QVERIFY(pointer->isValid());
1091     QSignalSpy enteredSpy(pointer, &Pointer::entered);
1092     QVERIFY(enteredSpy.isValid());
1093     QSignalSpy leftSpy(pointer, &Pointer::left);
1094     QVERIFY(leftSpy.isValid());
1095     // move cursor somewhere the new window won't open
1096     cursor->setPos(800, 800);
1097     // here we should have the fallback cursor
1098     const QImage fallback = cursor->image();
1099     QVERIFY(!fallback.isNull());
1100 
1101     // now let's create a window
1102     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
1103     QVERIFY(clientAddedSpy.isValid());
1104     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
1105     QVERIFY(surface);
1106     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
1107     QVERIFY(shellSurface);
1108     render(surface);
1109     QVERIFY(clientAddedSpy.wait());
1110     AbstractClient *window = workspace()->activeClient();
1111     QVERIFY(window);
1112 
1113     // and move cursor to the window
1114     QVERIFY(!window->frameGeometry().contains(QPoint(800, 800)));
1115     cursor->setPos(window->frameGeometry().center());
1116     QVERIFY(enteredSpy.wait());
1117     // cursor image should still be fallback
1118     QCOMPARE(cursor->image(), fallback);
1119 
1120     // now create an effect and set an override cursor
1121     QScopedPointer<HelperEffect> effect(new HelperEffect);
1122     effects->startMouseInterception(effect.data(), Qt::SizeAllCursor);
1123     const QImage sizeAll = cursor->image();
1124     QVERIFY(!sizeAll.isNull());
1125     QVERIFY(sizeAll != fallback);
1126     QVERIFY(leftSpy.wait());
1127 
1128     // let's change to arrow cursor, this should be our fallback
1129     effects->defineCursor(Qt::ArrowCursor);
1130     QCOMPARE(cursor->image(), fallback);
1131 
1132     // back to size all
1133     effects->defineCursor(Qt::SizeAllCursor);
1134     QCOMPARE(cursor->image(), sizeAll);
1135 
1136     // move cursor outside the window area
1137     Cursors::self()->mouse()->setPos(800, 800);
1138     // and end the override, which should switch to fallback
1139     effects->stopMouseInterception(effect.data());
1140     QCOMPARE(cursor->image(), fallback);
1141 
1142     // start mouse interception again
1143     effects->startMouseInterception(effect.data(), Qt::SizeAllCursor);
1144     QCOMPARE(cursor->image(), sizeAll);
1145 
1146     // move cursor to area of window
1147     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
1148     // this should not result in an enter event
1149     QVERIFY(!enteredSpy.wait(100));
1150 
1151     // after ending the interception we should get an enter event
1152     effects->stopMouseInterception(effect.data());
1153     QVERIFY(enteredSpy.wait());
1154     QVERIFY(cursor->image().isNull());
1155 }
1156 
testPopup()1157 void PointerInputTest::testPopup()
1158 {
1159     // this test validates the basic popup behavior
1160     // a button press outside the window should dismiss the popup
1161 
1162     // first create a parent surface
1163     using namespace KWayland::Client;
1164     auto pointer = m_seat->createPointer(m_seat);
1165     QVERIFY(pointer);
1166     QVERIFY(pointer->isValid());
1167     QSignalSpy enteredSpy(pointer, &Pointer::entered);
1168     QVERIFY(enteredSpy.isValid());
1169     QSignalSpy leftSpy(pointer, &Pointer::left);
1170     QVERIFY(leftSpy.isValid());
1171     QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged);
1172     QVERIFY(buttonStateChangedSpy.isValid());
1173     QSignalSpy motionSpy(pointer, &Pointer::motion);
1174     QVERIFY(motionSpy.isValid());
1175 
1176     Cursors::self()->mouse()->setPos(800, 800);
1177 
1178     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
1179     QVERIFY(clientAddedSpy.isValid());
1180     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
1181     QVERIFY(surface);
1182     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
1183     QVERIFY(shellSurface);
1184     render(surface);
1185     QVERIFY(clientAddedSpy.wait());
1186     AbstractClient *window = workspace()->activeClient();
1187     QVERIFY(window);
1188     QCOMPARE(window->hasPopupGrab(), false);
1189     // move pointer into window
1190     QVERIFY(!window->frameGeometry().contains(QPoint(800, 800)));
1191     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
1192     QVERIFY(enteredSpy.wait());
1193     // click inside window to create serial
1194     quint32 timestamp = 0;
1195     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
1196     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
1197     QVERIFY(buttonStateChangedSpy.wait());
1198 
1199     // now create the popup surface
1200     QScopedPointer<Test::XdgPositioner> positioner(Test::createXdgPositioner());
1201     positioner->set_size(100, 50);
1202     positioner->set_anchor_rect(0, 0, 80, 20);
1203     positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right);
1204     positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right);
1205     KWayland::Client::Surface *popupSurface = Test::createSurface(m_compositor);
1206     QVERIFY(popupSurface);
1207     Test::XdgPopup *popupShellSurface = Test::createXdgPopupSurface(popupSurface, shellSurface->xdgSurface(), positioner.data());
1208     QVERIFY(popupShellSurface);
1209     QSignalSpy doneReceivedSpy(popupShellSurface, &Test::XdgPopup::doneReceived);
1210     QVERIFY(doneReceivedSpy.isValid());
1211     popupShellSurface->grab(*Test::waylandSeat(), 0); // FIXME: Serial.
1212     render(popupSurface, QSize(100, 50));
1213     QVERIFY(clientAddedSpy.wait());
1214     auto popupClient = clientAddedSpy.last().first().value<AbstractClient *>();
1215     QVERIFY(popupClient);
1216     QVERIFY(popupClient != window);
1217     QCOMPARE(window, workspace()->activeClient());
1218     QCOMPARE(popupClient->transientFor(), window);
1219     QCOMPARE(popupClient->pos(), window->pos() + QPoint(80, 20));
1220     QCOMPARE(popupClient->hasPopupGrab(), true);
1221 
1222     // let's move the pointer into the center of the window
1223     Cursors::self()->mouse()->setPos(popupClient->frameGeometry().center());
1224     QVERIFY(enteredSpy.wait());
1225     QCOMPARE(enteredSpy.count(), 2);
1226     QCOMPARE(leftSpy.count(), 1);
1227     QCOMPARE(pointer->enteredSurface(), popupSurface);
1228 
1229     // let's move the pointer outside of the popup window
1230     // this should not really change anything, it gets a leave event
1231     Cursors::self()->mouse()->setPos(popupClient->frameGeometry().bottomRight() + QPoint(2, 2));
1232     QVERIFY(leftSpy.wait());
1233     QCOMPARE(leftSpy.count(), 2);
1234     QVERIFY(doneReceivedSpy.isEmpty());
1235     // now click, should trigger popupDone
1236     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
1237     QVERIFY(doneReceivedSpy.wait());
1238     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
1239 }
1240 
testDecoCancelsPopup()1241 void PointerInputTest::testDecoCancelsPopup()
1242 {
1243     // this test verifies that clicking the window decoration of parent window
1244     // cancels the popup
1245 
1246     // first create a parent surface
1247     using namespace KWayland::Client;
1248     auto pointer = m_seat->createPointer(m_seat);
1249     QVERIFY(pointer);
1250     QVERIFY(pointer->isValid());
1251     QSignalSpy enteredSpy(pointer, &Pointer::entered);
1252     QVERIFY(enteredSpy.isValid());
1253     QSignalSpy leftSpy(pointer, &Pointer::left);
1254     QVERIFY(leftSpy.isValid());
1255     QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged);
1256     QVERIFY(buttonStateChangedSpy.isValid());
1257     QSignalSpy motionSpy(pointer, &Pointer::motion);
1258     QVERIFY(motionSpy.isValid());
1259 
1260     Cursors::self()->mouse()->setPos(800, 800);
1261     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
1262     QVERIFY(clientAddedSpy.isValid());
1263     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
1264     QVERIFY(surface);
1265     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
1266     QVERIFY(shellSurface);
1267 
1268     auto deco = Test::waylandServerSideDecoration()->create(surface, surface);
1269     QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged);
1270     QVERIFY(decoSpy.isValid());
1271     QVERIFY(decoSpy.wait());
1272     deco->requestMode(ServerSideDecoration::Mode::Server);
1273     QVERIFY(decoSpy.wait());
1274     QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server);
1275     render(surface);
1276     QVERIFY(clientAddedSpy.wait());
1277     AbstractClient *window = workspace()->activeClient();
1278     QVERIFY(window);
1279     QCOMPARE(window->hasPopupGrab(), false);
1280     QVERIFY(window->isDecorated());
1281 
1282     // move pointer into window
1283     QVERIFY(!window->frameGeometry().contains(QPoint(800, 800)));
1284     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
1285     QVERIFY(enteredSpy.wait());
1286     // click inside window to create serial
1287     quint32 timestamp = 0;
1288     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
1289     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
1290     QVERIFY(buttonStateChangedSpy.wait());
1291 
1292     // now create the popup surface
1293     QScopedPointer<Test::XdgPositioner> positioner(Test::createXdgPositioner());
1294     positioner->set_size(100, 50);
1295     positioner->set_anchor_rect(0, 0, 80, 20);
1296     positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right);
1297     positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right);
1298     KWayland::Client::Surface *popupSurface = Test::createSurface(m_compositor);
1299     QVERIFY(popupSurface);
1300     Test::XdgPopup *popupShellSurface = Test::createXdgPopupSurface(popupSurface, shellSurface->xdgSurface(), positioner.data());
1301     QVERIFY(popupShellSurface);
1302     QSignalSpy doneReceivedSpy(popupShellSurface, &Test::XdgPopup::doneReceived);
1303     QVERIFY(doneReceivedSpy.isValid());
1304     popupShellSurface->grab(*Test::waylandSeat(), 0); // FIXME: Serial.
1305     render(popupSurface, QSize(100, 50));
1306     QVERIFY(clientAddedSpy.wait());
1307     auto popupClient = clientAddedSpy.last().first().value<AbstractClient *>();
1308     QVERIFY(popupClient);
1309     QVERIFY(popupClient != window);
1310     QCOMPARE(window, workspace()->activeClient());
1311     QCOMPARE(popupClient->transientFor(), window);
1312     QCOMPARE(popupClient->pos(), window->pos() + window->clientPos() + QPoint(80, 20));
1313     QCOMPARE(popupClient->hasPopupGrab(), true);
1314 
1315     // let's move the pointer into the center of the deco
1316     Cursors::self()->mouse()->setPos(window->frameGeometry().center().x(), window->y() + (window->height() - window->clientSize().height()) / 2);
1317 
1318     kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++);
1319     QVERIFY(doneReceivedSpy.wait());
1320     kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++);
1321 }
1322 
testWindowUnderCursorWhileButtonPressed()1323 void PointerInputTest::testWindowUnderCursorWhileButtonPressed()
1324 {
1325     // this test verifies that opening a window underneath the mouse cursor does not
1326     // trigger a leave event if a button is pressed
1327     // see BUG: 372876
1328 
1329     // first create a parent surface
1330     using namespace KWayland::Client;
1331     auto pointer = m_seat->createPointer(m_seat);
1332     QVERIFY(pointer);
1333     QVERIFY(pointer->isValid());
1334     QSignalSpy enteredSpy(pointer, &Pointer::entered);
1335     QVERIFY(enteredSpy.isValid());
1336     QSignalSpy leftSpy(pointer, &Pointer::left);
1337     QVERIFY(leftSpy.isValid());
1338 
1339     Cursors::self()->mouse()->setPos(800, 800);
1340     QSignalSpy clientAddedSpy(workspace(), &Workspace::clientAdded);
1341     QVERIFY(clientAddedSpy.isValid());
1342     KWayland::Client::Surface *surface = Test::createSurface(m_compositor);
1343     QVERIFY(surface);
1344     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
1345     QVERIFY(shellSurface);
1346     render(surface);
1347     QVERIFY(clientAddedSpy.wait());
1348     AbstractClient *window = workspace()->activeClient();
1349     QVERIFY(window);
1350 
1351     // move cursor over window
1352     QVERIFY(!window->frameGeometry().contains(QPoint(800, 800)));
1353     Cursors::self()->mouse()->setPos(window->frameGeometry().center());
1354     QVERIFY(enteredSpy.wait());
1355     // click inside window
1356     quint32 timestamp = 0;
1357     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
1358 
1359     // now create a second window as transient
1360     QScopedPointer<Test::XdgPositioner> positioner(Test::createXdgPositioner());
1361     positioner->set_size(99, 49);
1362     positioner->set_anchor_rect(0, 0, 1, 1);
1363     positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right);
1364     positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right);
1365     KWayland::Client::Surface *popupSurface = Test::createSurface(m_compositor);
1366     QVERIFY(popupSurface);
1367     Test::XdgPopup *popupShellSurface = Test::createXdgPopupSurface(popupSurface, shellSurface->xdgSurface(), positioner.data());
1368     QVERIFY(popupShellSurface);
1369     render(popupSurface, QSize(99, 49));
1370     QVERIFY(clientAddedSpy.wait());
1371     auto popupClient = clientAddedSpy.last().first().value<AbstractClient *>();
1372     QVERIFY(popupClient);
1373     QVERIFY(popupClient != window);
1374     QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos()));
1375     QVERIFY(popupClient->frameGeometry().contains(Cursors::self()->mouse()->pos()));
1376     QVERIFY(!leftSpy.wait());
1377 
1378     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
1379     // now that the button is no longer pressed we should get the leave event
1380     QVERIFY(leftSpy.wait());
1381     QCOMPARE(leftSpy.count(), 1);
1382     QCOMPARE(enteredSpy.count(), 2);
1383 }
1384 
testConfineToScreenGeometry_data()1385 void PointerInputTest::testConfineToScreenGeometry_data()
1386 {
1387     QTest::addColumn<QPoint>("startPos");
1388     QTest::addColumn<QPoint>("targetPos");
1389     QTest::addColumn<QPoint>("expectedPos");
1390 
1391     // screen layout:
1392     //
1393     // +----------+----------+---------+
1394     // |   left   |    top   |  right  |
1395     // +----------+----------+---------+
1396     //            |  bottom  |
1397     //            +----------+
1398     //
1399 
1400     QTest::newRow("move top-left - left screen")       << QPoint(640, 512)   << QPoint(-100, -100) << QPoint(0, 0);
1401     QTest::newRow("move top - left screen")            << QPoint(640, 512)   << QPoint(640, -100)  << QPoint(640, 0);
1402     QTest::newRow("move top-right - left screen")      << QPoint(640, 512)   << QPoint(1380, -100) << QPoint(1380, 0);
1403     QTest::newRow("move right - left screen")          << QPoint(640, 512)   << QPoint(1380, 512)  << QPoint(1380, 512);
1404     QTest::newRow("move bottom-right - left screen")   << QPoint(640, 512)   << QPoint(1380, 1124) << QPoint(1380, 1124);
1405     QTest::newRow("move bottom - left screen")         << QPoint(640, 512)   << QPoint(640, 1124)  << QPoint(640, 1023);
1406     QTest::newRow("move bottom-left - left screen")    << QPoint(640, 512)   << QPoint(-100, 1124) << QPoint(0, 1023);
1407     QTest::newRow("move left - left screen")           << QPoint(640, 512)   << QPoint(-100, 512)  << QPoint(0, 512);
1408 
1409     QTest::newRow("move top-left - top screen")        << QPoint(1920, 512)  << QPoint(1180, -100) << QPoint(1180, 0);
1410     QTest::newRow("move top - top screen")             << QPoint(1920, 512)  << QPoint(1920, -100) << QPoint(1920, 0);
1411     QTest::newRow("move top-right - top screen")       << QPoint(1920, 512)  << QPoint(2660, -100) << QPoint(2660, 0);
1412     QTest::newRow("move right - top screen")           << QPoint(1920, 512)  << QPoint(2660, 512)  << QPoint(2660, 512);
1413     QTest::newRow("move bottom-right - top screen")    << QPoint(1920, 512)  << QPoint(2660, 1124) << QPoint(2559, 1023);
1414     QTest::newRow("move bottom - top screen")          << QPoint(1920, 512)  << QPoint(1920, 1124) << QPoint(1920, 1124);
1415     QTest::newRow("move bottom-left - top screen")     << QPoint(1920, 512)  << QPoint(1180, 1124) << QPoint(1280, 1023);
1416     QTest::newRow("move left - top screen")            << QPoint(1920, 512)  << QPoint(1180, 512)  << QPoint(1180, 512);
1417 
1418     QTest::newRow("move top-left - right screen")      << QPoint(3200, 512)  << QPoint(2460, -100) << QPoint(2460, 0);
1419     QTest::newRow("move top - right screen")           << QPoint(3200, 512)  << QPoint(3200, -100) << QPoint(3200, 0);
1420     QTest::newRow("move top-right - right screen")     << QPoint(3200, 512)  << QPoint(3940, -100) << QPoint(3839, 0);
1421     QTest::newRow("move right - right screen")         << QPoint(3200, 512)  << QPoint(3940, 512)  << QPoint(3839, 512);
1422     QTest::newRow("move bottom-right - right screen")  << QPoint(3200, 512)  << QPoint(3940, 1124) << QPoint(3839, 1023);
1423     QTest::newRow("move bottom - right screen")        << QPoint(3200, 512)  << QPoint(3200, 1124) << QPoint(3200, 1023);
1424     QTest::newRow("move bottom-left - right screen")   << QPoint(3200, 512)  << QPoint(2460, 1124) << QPoint(2460, 1124);
1425     QTest::newRow("move left - right screen")          << QPoint(3200, 512)  << QPoint(2460, 512)  << QPoint(2460, 512);
1426 
1427     QTest::newRow("move top-left - bottom screen")     << QPoint(1920, 1536) << QPoint(1180, 924)  << QPoint(1180, 924);
1428     QTest::newRow("move top - bottom screen")          << QPoint(1920, 1536) << QPoint(1920, 924)  << QPoint(1920, 924);
1429     QTest::newRow("move top-right - bottom screen")    << QPoint(1920, 1536) << QPoint(2660, 924)  << QPoint(2660, 924);
1430     QTest::newRow("move right - bottom screen")        << QPoint(1920, 1536) << QPoint(2660, 1536) << QPoint(2559, 1536);
1431     QTest::newRow("move bottom-right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 2148) << QPoint(2559, 2047);
1432     QTest::newRow("move bottom - bottom screen")       << QPoint(1920, 1536) << QPoint(1920, 2148) << QPoint(1920, 2047);
1433     QTest::newRow("move bottom-left - bottom screen")  << QPoint(1920, 1536) << QPoint(1180, 2148) << QPoint(1280, 2047);
1434     QTest::newRow("move left - bottom screen")         << QPoint(1920, 1536) << QPoint(1180, 1536) << QPoint(1280, 1536);
1435 }
1436 
testConfineToScreenGeometry()1437 void PointerInputTest::testConfineToScreenGeometry()
1438 {
1439     // this test verifies that pointer belongs to at least one screen
1440     // after moving it to off-screen area
1441 
1442     // unload the Present Windows effect because it pushes back
1443     // pointer if it's at (0, 0)
1444     static_cast<EffectsHandlerImpl*>(effects)->unloadEffect(QStringLiteral("presentwindows"));
1445 
1446     // setup screen layout
1447     const QVector<QRect> geometries {
1448         QRect(0, 0, 1280, 1024),
1449         QRect(1280, 0, 1280, 1024),
1450         QRect(2560, 0, 1280, 1024),
1451         QRect(1280, 1024, 1280, 1024)
1452     };
1453     QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs",
1454                               Qt::DirectConnection,
1455                               Q_ARG(int, geometries.count()),
1456                               Q_ARG(QVector<QRect>, geometries));
1457     QCOMPARE(screens()->count(), geometries.count());
1458     QCOMPARE(screens()->geometry(0), geometries.at(0));
1459     QCOMPARE(screens()->geometry(1), geometries.at(1));
1460     QCOMPARE(screens()->geometry(2), geometries.at(2));
1461     QCOMPARE(screens()->geometry(3), geometries.at(3));
1462 
1463     // move pointer to initial position
1464     QFETCH(QPoint, startPos);
1465     Cursors::self()->mouse()->setPos(startPos);
1466     QCOMPARE(Cursors::self()->mouse()->pos(), startPos);
1467 
1468     // perform movement
1469     QFETCH(QPoint, targetPos);
1470     kwinApp()->platform()->pointerMotion(targetPos, 1);
1471 
1472     QFETCH(QPoint, expectedPos);
1473     QCOMPARE(Cursors::self()->mouse()->pos(), expectedPos);
1474 }
1475 
testResizeCursor_data()1476 void PointerInputTest::testResizeCursor_data()
1477 {
1478     QTest::addColumn<Qt::Edges>("edges");
1479     QTest::addColumn<KWin::CursorShape>("cursorShape");
1480 
1481     QTest::newRow("top-left")     << Qt::Edges(Qt::TopEdge | Qt::LeftEdge)     << CursorShape(ExtendedCursor::SizeNorthWest);
1482     QTest::newRow("top")          << Qt::Edges(Qt::TopEdge)                    << CursorShape(ExtendedCursor::SizeNorth);
1483     QTest::newRow("top-right")    << Qt::Edges(Qt::TopEdge | Qt::RightEdge)    << CursorShape(ExtendedCursor::SizeNorthEast);
1484     QTest::newRow("right")        << Qt::Edges(Qt::RightEdge)                  << CursorShape(ExtendedCursor::SizeEast);
1485     QTest::newRow("bottom-right") << Qt::Edges(Qt::BottomEdge | Qt::RightEdge) << CursorShape(ExtendedCursor::SizeSouthEast);
1486     QTest::newRow("bottom")       << Qt::Edges(Qt::BottomEdge)                 << CursorShape(ExtendedCursor::SizeSouth);
1487     QTest::newRow("bottom-left")  << Qt::Edges(Qt::BottomEdge | Qt::LeftEdge)  << CursorShape(ExtendedCursor::SizeSouthWest);
1488     QTest::newRow("left")         << Qt::Edges(Qt::LeftEdge)                   << CursorShape(ExtendedCursor::SizeWest);
1489 }
1490 
testResizeCursor()1491 void PointerInputTest::testResizeCursor()
1492 {
1493     // this test verifies that the cursor has correct shape during resize operation
1494 
1495     // first modify the config for this run
1496     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
1497     group.writeEntry("CommandAllKey", "Meta");
1498     group.writeEntry("CommandAll3", "Resize");
1499     group.sync();
1500     workspace()->slotReconfigure();
1501     QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
1502     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedResize);
1503 
1504     // load the fallback cursor (arrow cursor)
1505     const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor);
1506     QVERIFY(!arrowCursor.isNull());
1507     QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image());
1508     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot());
1509 
1510     // we need a pointer to get the enter event
1511     auto pointer = m_seat->createPointer(m_seat);
1512     QVERIFY(pointer);
1513     QVERIFY(pointer->isValid());
1514     QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered);
1515     QVERIFY(enteredSpy.isValid());
1516 
1517     // create a test client
1518     using namespace KWayland::Client;
1519     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
1520     QVERIFY(!surface.isNull());
1521     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
1522     QVERIFY(!shellSurface.isNull());
1523     AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue);
1524     QVERIFY(c);
1525 
1526     // move the cursor to the test position
1527     QPoint cursorPos;
1528     QFETCH(Qt::Edges, edges);
1529 
1530     if (edges & Qt::LeftEdge) {
1531         cursorPos.setX(c->frameGeometry().left());
1532     } else if (edges & Qt::RightEdge) {
1533         cursorPos.setX(c->frameGeometry().right());
1534     } else {
1535         cursorPos.setX(c->frameGeometry().center().x());
1536     }
1537 
1538     if (edges & Qt::TopEdge) {
1539         cursorPos.setY(c->frameGeometry().top());
1540     } else if (edges & Qt::BottomEdge) {
1541         cursorPos.setY(c->frameGeometry().bottom());
1542     } else {
1543         cursorPos.setY(c->frameGeometry().center().y());
1544     }
1545 
1546     Cursors::self()->mouse()->setPos(cursorPos);
1547 
1548     // wait for the enter event and set the cursor
1549     QVERIFY(enteredSpy.wait());
1550     QScopedPointer<KWayland::Client::Surface> cursorSurface(Test::createSurface(m_compositor));
1551     QVERIFY(cursorSurface);
1552     QSignalSpy cursorRenderedSpy(cursorSurface.data(), &KWayland::Client::Surface::frameRendered);
1553     QVERIFY(cursorRenderedSpy.isValid());
1554     cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(arrowCursor.image()));
1555     cursorSurface->damage(arrowCursor.image().rect());
1556     cursorSurface->commit();
1557     pointer->setCursor(cursorSurface.data(), arrowCursor.hotSpot());
1558     QVERIFY(cursorRenderedSpy.wait());
1559 
1560     // start resizing the client
1561     int timestamp = 1;
1562     kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++);
1563     kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++);
1564     QVERIFY(c->isInteractiveResize());
1565 
1566     QFETCH(KWin::CursorShape, cursorShape);
1567     const PlatformCursorImage resizeCursor = loadReferenceThemeCursor(cursorShape);
1568     QVERIFY(!resizeCursor.isNull());
1569     QCOMPARE(kwinApp()->platform()->cursorImage().image(), resizeCursor.image());
1570     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), resizeCursor.hotSpot());
1571 
1572     // finish resizing the client
1573     kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++);
1574     kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++);
1575     QVERIFY(!c->isInteractiveResize());
1576 
1577     QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image());
1578     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot());
1579 }
1580 
testMoveCursor()1581 void PointerInputTest::testMoveCursor()
1582 {
1583     // this test verifies that the cursor has correct shape during move operation
1584 
1585     // first modify the config for this run
1586     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
1587     group.writeEntry("CommandAllKey", "Meta");
1588     group.writeEntry("CommandAll1", "Move");
1589     group.sync();
1590     workspace()->slotReconfigure();
1591     QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
1592     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
1593 
1594     // load the fallback cursor (arrow cursor)
1595     const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor);
1596     QVERIFY(!arrowCursor.isNull());
1597     QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image());
1598     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot());
1599 
1600     // we need a pointer to get the enter event
1601     auto pointer = m_seat->createPointer(m_seat);
1602     QVERIFY(pointer);
1603     QVERIFY(pointer->isValid());
1604     QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered);
1605     QVERIFY(enteredSpy.isValid());
1606 
1607     // create a test client
1608     using namespace KWayland::Client;
1609     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
1610     QVERIFY(!surface.isNull());
1611     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
1612     QVERIFY(!shellSurface.isNull());
1613     AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue);
1614     QVERIFY(c);
1615 
1616     // move cursor to the test position
1617     Cursors::self()->mouse()->setPos(c->frameGeometry().center());
1618 
1619     // wait for the enter event and set the cursor
1620     QVERIFY(enteredSpy.wait());
1621     QScopedPointer<KWayland::Client::Surface> cursorSurface(Test::createSurface(m_compositor));
1622     QVERIFY(cursorSurface);
1623     QSignalSpy cursorRenderedSpy(cursorSurface.data(), &KWayland::Client::Surface::frameRendered);
1624     QVERIFY(cursorRenderedSpy.isValid());
1625     cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(arrowCursor.image()));
1626     cursorSurface->damage(arrowCursor.image().rect());
1627     cursorSurface->commit();
1628     pointer->setCursor(cursorSurface.data(), arrowCursor.hotSpot());
1629     QVERIFY(cursorRenderedSpy.wait());
1630 
1631     // start moving the client
1632     int timestamp = 1;
1633     kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTMETA, timestamp++);
1634     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
1635     QVERIFY(c->isInteractiveMove());
1636 
1637     const PlatformCursorImage sizeAllCursor = loadReferenceThemeCursor(Qt::SizeAllCursor);
1638     QVERIFY(!sizeAllCursor.isNull());
1639     QCOMPARE(kwinApp()->platform()->cursorImage().image(), sizeAllCursor.image());
1640     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), sizeAllCursor.hotSpot());
1641 
1642     // finish moving the client
1643     kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTMETA, timestamp++);
1644     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
1645     QVERIFY(!c->isInteractiveMove());
1646 
1647     QCOMPARE(kwinApp()->platform()->cursorImage().image(), arrowCursor.image());
1648     QCOMPARE(kwinApp()->platform()->cursorImage().hotSpot(), arrowCursor.hotSpot());
1649 }
1650 
testHideShowCursor()1651 void PointerInputTest::testHideShowCursor()
1652 {
1653     QCOMPARE(kwinApp()->platform()->isCursorHidden(), false);
1654     kwinApp()->platform()->hideCursor();
1655     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1656     kwinApp()->platform()->showCursor();
1657     QCOMPARE(kwinApp()->platform()->isCursorHidden(), false);
1658 
1659     kwinApp()->platform()->hideCursor();
1660     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1661     kwinApp()->platform()->hideCursor();
1662     kwinApp()->platform()->hideCursor();
1663     kwinApp()->platform()->hideCursor();
1664     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1665 
1666     kwinApp()->platform()->showCursor();
1667     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1668     kwinApp()->platform()->showCursor();
1669     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1670     kwinApp()->platform()->showCursor();
1671     QCOMPARE(kwinApp()->platform()->isCursorHidden(), true);
1672     kwinApp()->platform()->showCursor();
1673     QCOMPARE(kwinApp()->platform()->isCursorHidden(), false);
1674 }
1675 
testDefaultInputRegion()1676 void PointerInputTest::testDefaultInputRegion()
1677 {
1678     // This test verifies that a surface that hasn't specified the input region can be focused.
1679 
1680     // Create a test client.
1681     using namespace KWayland::Client;
1682     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
1683     QVERIFY(!surface.isNull());
1684     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
1685     QVERIFY(!shellSurface.isNull());
1686     AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue);
1687     QVERIFY(client);
1688 
1689     // Move the point to the center of the surface.
1690     Cursors::self()->mouse()->setPos(client->frameGeometry().center());
1691     QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), client->surface());
1692 
1693     // Destroy the test client.
1694     shellSurface.reset();
1695     QVERIFY(Test::waitForWindowDestroyed(client));
1696 }
1697 
testEmptyInputRegion()1698 void PointerInputTest::testEmptyInputRegion()
1699 {
1700     // This test verifies that a surface that has specified an empty input region can't be focused.
1701 
1702     // Create a test client.
1703     using namespace KWayland::Client;
1704     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
1705     QVERIFY(!surface.isNull());
1706     std::unique_ptr<KWayland::Client::Region> inputRegion(m_compositor->createRegion(QRegion()));
1707     surface->setInputRegion(inputRegion.get());
1708     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
1709     QVERIFY(!shellSurface.isNull());
1710     AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue);
1711     QVERIFY(client);
1712 
1713     // Move the point to the center of the surface.
1714     Cursors::self()->mouse()->setPos(client->frameGeometry().center());
1715     QVERIFY(!waylandServer()->seat()->focusedPointerSurface());
1716 
1717     // Destroy the test client.
1718     shellSurface.reset();
1719     QVERIFY(Test::waitForWindowDestroyed(client));
1720 }
1721 
1722 }
1723 
1724 WAYLANDTEST_MAIN(KWin::PointerInputTest)
1725 #include "pointer_input.moc"
1726