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