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_client.h"
11 #include "abstract_output.h"
12 #include "cursor.h"
13 #include "internal_client.h"
14 #include "platform.h"
15 #include "pointer_input.h"
16 #include "touch_input.h"
17 #include "screenedge.h"
18 #include "screens.h"
19 #include "wayland_server.h"
20 #include "workspace.h"
21 #include <kwineffects.h>
22 
23 #include "decorations/decoratedclient.h"
24 #include "decorations/decorationbridge.h"
25 #include "decorations/settings.h"
26 
27 #include <KWayland/Client/connection_thread.h>
28 #include <KWayland/Client/compositor.h>
29 #include <KWayland/Client/keyboard.h>
30 #include <KWayland/Client/pointer.h>
31 #include <KWayland/Client/server_decoration.h>
32 #include <KWayland/Client/seat.h>
33 #include <KWayland/Client/shm_pool.h>
34 #include <KWayland/Client/surface.h>
35 
36 #include <KDecoration2/Decoration>
37 #include <KDecoration2/DecorationSettings>
38 
39 #include <linux/input.h>
40 
41 Q_DECLARE_METATYPE(Qt::WindowFrameSection)
42 
43 namespace KWin
44 {
45 
46 static const QString s_socketName = QStringLiteral("wayland_test_kwin_decoration_input-0");
47 
48 class DecorationInputTest : public QObject
49 {
50     Q_OBJECT
51 private Q_SLOTS:
52     void initTestCase();
53     void init();
54     void cleanup();
55     void testAxis_data();
56     void testAxis();
57     void testDoubleClick_data();
58     void testDoubleClick();
59     void testDoubleTap_data();
60     void testDoubleTap();
61     void testHover();
62     void testPressToMove_data();
63     void testPressToMove();
64     void testTapToMove_data();
65     void testTapToMove();
66     void testResizeOutsideWindow_data();
67     void testResizeOutsideWindow();
68     void testModifierClickUnrestrictedMove_data();
69     void testModifierClickUnrestrictedMove();
70     void testModifierScrollOpacity_data();
71     void testModifierScrollOpacity();
72     void testTouchEvents();
73     void testTooltipDoesntEatKeyEvents();
74 
75 private:
76     AbstractClient *showWindow();
77 };
78 
79 #define MOTION(target) \
80     kwinApp()->platform()->pointerMotion(target, timestamp++)
81 
82 #define PRESS \
83     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++)
84 
85 #define RELEASE \
86     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++)
87 
showWindow()88 AbstractClient *DecorationInputTest::showWindow()
89 {
90     using namespace KWayland::Client;
91 #define VERIFY(statement) \
92     if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\
93         return nullptr;
94 #define COMPARE(actual, expected) \
95     if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\
96         return nullptr;
97 
98     KWayland::Client::Surface *surface = Test::createSurface(Test::waylandCompositor());
99     VERIFY(surface);
100     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface, surface);
101     VERIFY(shellSurface);
102     auto deco = Test::waylandServerSideDecoration()->create(surface, surface);
103     QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged);
104     VERIFY(decoSpy.isValid());
105     VERIFY(decoSpy.wait());
106     deco->requestMode(ServerSideDecoration::Mode::Server);
107     VERIFY(decoSpy.wait());
108     COMPARE(deco->mode(), ServerSideDecoration::Mode::Server);
109     // let's render
110     auto c = Test::renderAndWaitForShown(surface, QSize(500, 50), Qt::blue);
111     VERIFY(c);
112     COMPARE(workspace()->activeClient(), c);
113 
114 #undef VERIFY
115 #undef COMPARE
116 
117     return c;
118 }
119 
initTestCase()120 void DecorationInputTest::initTestCase()
121 {
122     qRegisterMetaType<KWin::AbstractClient *>();
123     qRegisterMetaType<KWin::InternalClient *>();
124     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
125     QVERIFY(applicationStartedSpy.isValid());
126     kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
127     QVERIFY(waylandServer()->init(s_socketName));
128     QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2));
129 
130     // change some options
131     KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
132     config->group(QStringLiteral("MouseBindings")).writeEntry("CommandTitlebarWheel", QStringLiteral("above/below"));
133     config->group(QStringLiteral("Windows")).writeEntry("TitlebarDoubleClickCommand", QStringLiteral("OnAllDesktops"));
134     config->group(QStringLiteral("Desktops")).writeEntry("Number", 2);
135     config->sync();
136 
137     kwinApp()->setConfig(config);
138 
139     kwinApp()->start();
140     QVERIFY(applicationStartedSpy.wait());
141     const auto outputs = kwinApp()->platform()->enabledOutputs();
142     QCOMPARE(outputs.count(), 2);
143     QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
144     QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
145     setenv("QT_QPA_PLATFORM", "wayland", true);
146     Test::initWaylandWorkspace();
147 }
148 
init()149 void DecorationInputTest::init()
150 {
151     using namespace KWayland::Client;
152     QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration));
153     QVERIFY(Test::waitForWaylandPointer());
154 
155     workspace()->setActiveOutput(QPoint(640, 512));
156     Cursors::self()->mouse()->setPos(QPoint(640, 512));
157 }
158 
cleanup()159 void DecorationInputTest::cleanup()
160 {
161     Test::destroyWaylandConnection();
162 }
163 
testAxis_data()164 void DecorationInputTest::testAxis_data()
165 {
166     QTest::addColumn<QPoint>("decoPoint");
167     QTest::addColumn<Qt::WindowFrameSection>("expectedSection");
168 
169     QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection;
170     QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection;
171     QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection;
172 }
173 
testAxis()174 void DecorationInputTest::testAxis()
175 {
176     AbstractClient *c = showWindow();
177     QVERIFY(c);
178     QVERIFY(c->isDecorated());
179     QVERIFY(!c->noBorder());
180     QCOMPARE(c->titlebarPosition(), AbstractClient::PositionTop);
181     QVERIFY(!c->keepAbove());
182     QVERIFY(!c->keepBelow());
183 
184     quint32 timestamp = 1;
185     MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2));
186     QVERIFY(input()->pointer()->decoration());
187     QCOMPARE(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), Qt::TitleBarArea);
188 
189     // TODO: mouse wheel direction looks wrong to me
190     // simulate wheel
191     kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++);
192     QVERIFY(c->keepBelow());
193     QVERIFY(!c->keepAbove());
194     kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++);
195     QVERIFY(!c->keepBelow());
196     QVERIFY(!c->keepAbove());
197     kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++);
198     QVERIFY(!c->keepBelow());
199     QVERIFY(c->keepAbove());
200 
201     // test top most deco pixel, BUG: 362860
202     c->move(QPoint(0, 0));
203     QFETCH(QPoint, decoPoint);
204     MOTION(decoPoint);
205     QVERIFY(input()->pointer()->decoration());
206     QCOMPARE(input()->pointer()->decoration()->client(), c);
207     QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection");
208     kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++);
209     QVERIFY(!c->keepBelow());
210     QVERIFY(!c->keepAbove());
211 }
212 
testDoubleClick_data()213 void DecorationInputTest::testDoubleClick_data()
214 {
215     QTest::addColumn<QPoint>("decoPoint");
216     QTest::addColumn<Qt::WindowFrameSection>("expectedSection");
217 
218     QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection;
219     QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection;
220     QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection;
221 }
222 
testDoubleClick()223 void KWin::DecorationInputTest::testDoubleClick()
224 {
225     AbstractClient *c = showWindow();
226     QVERIFY(c);
227     QVERIFY(c->isDecorated());
228     QVERIFY(!c->noBorder());
229     QVERIFY(!c->isOnAllDesktops());
230     quint32 timestamp = 1;
231     MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2));
232 
233     // double click
234     PRESS;
235     RELEASE;
236     PRESS;
237     RELEASE;
238     QVERIFY(c->isOnAllDesktops());
239     // double click again
240     PRESS;
241     RELEASE;
242     QVERIFY(c->isOnAllDesktops());
243     PRESS;
244     RELEASE;
245     QVERIFY(!c->isOnAllDesktops());
246 
247     // test top most deco pixel, BUG: 362860
248     c->move(QPoint(0, 0));
249     QFETCH(QPoint, decoPoint);
250     MOTION(decoPoint);
251     QVERIFY(input()->pointer()->decoration());
252     QCOMPARE(input()->pointer()->decoration()->client(), c);
253     QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection");
254     // double click
255     PRESS;
256     RELEASE;
257     QVERIFY(!c->isOnAllDesktops());
258     PRESS;
259     RELEASE;
260     QVERIFY(c->isOnAllDesktops());
261 }
262 
testDoubleTap_data()263 void DecorationInputTest::testDoubleTap_data()
264 {
265     QTest::addColumn<QPoint>("decoPoint");
266     QTest::addColumn<Qt::WindowFrameSection>("expectedSection");
267 
268     QTest::newRow("topLeft") << QPoint(10, 10) << Qt::TopLeftSection;
269     QTest::newRow("top") << QPoint(260, 10) << Qt::TopSection;
270     QTest::newRow("topRight") << QPoint(509, 10) << Qt::TopRightSection;
271 }
272 
testDoubleTap()273 void KWin::DecorationInputTest::testDoubleTap()
274 {
275     AbstractClient *c = showWindow();
276     QVERIFY(c);
277     QVERIFY(c->isDecorated());
278     QVERIFY(!c->noBorder());
279     QVERIFY(!c->isOnAllDesktops());
280     quint32 timestamp = 1;
281     const QPoint tapPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2);
282 
283     // double tap
284     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
285     kwinApp()->platform()->touchUp(0, timestamp++);
286     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
287     kwinApp()->platform()->touchUp(0, timestamp++);
288     QVERIFY(c->isOnAllDesktops());
289     // double tap again
290     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
291     kwinApp()->platform()->touchUp(0, timestamp++);
292     QVERIFY(c->isOnAllDesktops());
293     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
294     kwinApp()->platform()->touchUp(0, timestamp++);
295     QVERIFY(!c->isOnAllDesktops());
296 
297     // test top most deco pixel, BUG: 362860
298     //
299     // Not directly at (0, 0), otherwise ScreenEdgeInputFilter catches
300     // event before DecorationEventFilter.
301     c->move(QPoint(10, 10));
302     QFETCH(QPoint, decoPoint);
303     // double click
304     kwinApp()->platform()->touchDown(0, decoPoint, timestamp++);
305     QVERIFY(input()->touch()->decoration());
306     QCOMPARE(input()->touch()->decoration()->client(), c);
307     QTEST(input()->touch()->decoration()->decoration()->sectionUnderMouse(), "expectedSection");
308     kwinApp()->platform()->touchUp(0, timestamp++);
309     QVERIFY(!c->isOnAllDesktops());
310     kwinApp()->platform()->touchDown(0, decoPoint, timestamp++);
311     kwinApp()->platform()->touchUp(0, timestamp++);
312     QVERIFY(c->isOnAllDesktops());
313 }
314 
testHover()315 void DecorationInputTest::testHover()
316 {
317     AbstractClient *c = showWindow();
318     QVERIFY(c);
319     QVERIFY(c->isDecorated());
320     QVERIFY(!c->noBorder());
321 
322     // our left border is moved out of the visible area, so move the window to a better place
323     c->move(QPoint(20, 0));
324 
325     quint32 timestamp = 1;
326     MOTION(QPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2));
327     QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor));
328 
329     // There is a mismatch of the cursor key positions between windows
330     // with and without borders (with borders one can move inside a bit and still
331     // be on an edge, without not). We should make this consistent in KWin's core.
332     //
333     // TODO: Test input position with different border sizes.
334     // TODO: We should test with the fake decoration to have a fixed test environment.
335     const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None;
336     auto deviation = [hasBorders] {
337         return hasBorders ? -1 : 0;
338     };
339 
340     MOTION(QPoint(c->frameGeometry().x(), 0));
341     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthWest));
342     MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() / 2, 0));
343     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorth));
344     MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() - 1, 0));
345     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthEast));
346     MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() + deviation(), c->height() / 2));
347     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeEast));
348     MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() + deviation(), c->height() - 1));
349     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthEast));
350     MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() / 2, c->height() + deviation()));
351     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouth));
352     MOTION(QPoint(c->frameGeometry().x(), c->height() + deviation()));
353     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthWest));
354     MOTION(QPoint(c->frameGeometry().x() - 1, c->height() / 2));
355     QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeWest));
356 
357     MOTION(c->frameGeometry().center());
358     QEXPECT_FAIL("", "Cursor not set back on leave", Continue);
359     QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor));
360 }
361 
testPressToMove_data()362 void DecorationInputTest::testPressToMove_data()
363 {
364     QTest::addColumn<QPoint>("offset");
365     QTest::addColumn<QPoint>("offset2");
366     QTest::addColumn<QPoint>("offset3");
367 
368     QTest::newRow("To right")  << QPoint(10, 0)  << QPoint(20, 0)  << QPoint(30, 0);
369     QTest::newRow("To left")   << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0);
370     QTest::newRow("To bottom") << QPoint(0, 10)  << QPoint(0, 20)  << QPoint(0, 30);
371     QTest::newRow("To top")    << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30);
372 }
373 
testPressToMove()374 void DecorationInputTest::testPressToMove()
375 {
376     AbstractClient *c = showWindow();
377     QVERIFY(c);
378     QVERIFY(c->isDecorated());
379     QVERIFY(!c->noBorder());
380     c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2));
381     QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized);
382     QVERIFY(startMoveResizedSpy.isValid());
383     QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized);
384     QVERIFY(clientFinishUserMovedResizedSpy.isValid());
385 
386     quint32 timestamp = 1;
387     MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2));
388     QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor));
389 
390     PRESS;
391     QVERIFY(!c->isInteractiveMove());
392     QFETCH(QPoint, offset);
393     MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset);
394     const QPoint oldPos = c->pos();
395     QVERIFY(c->isInteractiveMove());
396     QCOMPARE(startMoveResizedSpy.count(), 1);
397 
398     RELEASE;
399     QTRY_VERIFY(!c->isInteractiveMove());
400     QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1);
401     QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue);
402     QCOMPARE(c->pos(), oldPos + offset);
403 
404     // again
405     PRESS;
406     QVERIFY(!c->isInteractiveMove());
407     QFETCH(QPoint, offset2);
408     MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2);
409     QVERIFY(c->isInteractiveMove());
410     QCOMPARE(startMoveResizedSpy.count(), 2);
411     QFETCH(QPoint, offset3);
412     MOTION(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3);
413 
414     RELEASE;
415     QTRY_VERIFY(!c->isInteractiveMove());
416     QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2);
417     // TODO: the offset should also be included
418     QCOMPARE(c->pos(), oldPos + offset2 + offset3);
419 }
420 
testTapToMove_data()421 void DecorationInputTest::testTapToMove_data()
422 {
423     QTest::addColumn<QPoint>("offset");
424     QTest::addColumn<QPoint>("offset2");
425     QTest::addColumn<QPoint>("offset3");
426 
427     QTest::newRow("To right")  << QPoint(10, 0)  << QPoint(20, 0)  << QPoint(30, 0);
428     QTest::newRow("To left")   << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0);
429     QTest::newRow("To bottom") << QPoint(0, 10)  << QPoint(0, 20)  << QPoint(0, 30);
430     QTest::newRow("To top")    << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30);
431 }
432 
testTapToMove()433 void DecorationInputTest::testTapToMove()
434 {
435     AbstractClient *c = showWindow();
436     QVERIFY(c);
437     QVERIFY(c->isDecorated());
438     QVERIFY(!c->noBorder());
439     c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2));
440     QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized);
441     QVERIFY(startMoveResizedSpy.isValid());
442     QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized);
443     QVERIFY(clientFinishUserMovedResizedSpy.isValid());
444 
445     quint32 timestamp = 1;
446     QPoint p = QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2);
447 
448     kwinApp()->platform()->touchDown(0, p, timestamp++);
449     QVERIFY(!c->isInteractiveMove());
450     QFETCH(QPoint, offset);
451     QCOMPARE(input()->touch()->decorationPressId(), 0);
452     kwinApp()->platform()->touchMotion(0, p + offset, timestamp++);
453     const QPoint oldPos = c->pos();
454     QVERIFY(c->isInteractiveMove());
455     QCOMPARE(startMoveResizedSpy.count(), 1);
456 
457     kwinApp()->platform()->touchUp(0, timestamp++);
458     QTRY_VERIFY(!c->isInteractiveMove());
459     QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1);
460     QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue);
461     QCOMPARE(c->pos(), oldPos + offset);
462 
463     // again
464     kwinApp()->platform()->touchDown(1, p + offset, timestamp++);
465     QCOMPARE(input()->touch()->decorationPressId(), 1);
466     QVERIFY(!c->isInteractiveMove());
467     QFETCH(QPoint, offset2);
468     kwinApp()->platform()->touchMotion(1, QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2, timestamp++);
469     QVERIFY(c->isInteractiveMove());
470     QCOMPARE(startMoveResizedSpy.count(), 2);
471     QFETCH(QPoint, offset3);
472     kwinApp()->platform()->touchMotion(1, QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3, timestamp++);
473 
474     kwinApp()->platform()->touchUp(1, timestamp++);
475     QTRY_VERIFY(!c->isInteractiveMove());
476     QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2);
477     // TODO: the offset should also be included
478     QCOMPARE(c->pos(), oldPos + offset2 + offset3);
479 }
480 
testResizeOutsideWindow_data()481 void DecorationInputTest::testResizeOutsideWindow_data()
482 {
483     QTest::addColumn<Qt::Edge>("edge");
484     QTest::addColumn<Qt::CursorShape>("expectedCursor");
485 
486     QTest::newRow("left") << Qt::LeftEdge << Qt::SizeHorCursor;
487     QTest::newRow("right") << Qt::RightEdge << Qt::SizeHorCursor;
488     QTest::newRow("bottom") << Qt::BottomEdge << Qt::SizeVerCursor;
489 }
490 
testResizeOutsideWindow()491 void DecorationInputTest::testResizeOutsideWindow()
492 {
493     // this test verifies that one can resize the window outside the decoration with NoSideBorder
494 
495     // first adjust config
496     kwinApp()->config()->group("org.kde.kdecoration2").writeEntry("BorderSize", QStringLiteral("None"));
497     kwinApp()->config()->sync();
498     workspace()->slotReconfigure();
499 
500     // now create window
501     AbstractClient *c = showWindow();
502     QVERIFY(c);
503     QVERIFY(c->isDecorated());
504     QVERIFY(!c->noBorder());
505     c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2));
506     QVERIFY(c->frameGeometry() != c->inputGeometry());
507     QVERIFY(c->inputGeometry().contains(c->frameGeometry()));
508     QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized);
509     QVERIFY(startMoveResizedSpy.isValid());
510 
511     // go to border
512     quint32 timestamp = 1;
513     QFETCH(Qt::Edge, edge);
514     switch (edge) {
515     case Qt::LeftEdge:
516         MOTION(QPoint(c->frameGeometry().x() -1, c->frameGeometry().center().y()));
517         break;
518     case Qt::RightEdge:
519         MOTION(QPoint(c->frameGeometry().x() + c->frameGeometry().width() +1, c->frameGeometry().center().y()));
520         break;
521     case Qt::BottomEdge:
522         MOTION(QPoint(c->frameGeometry().center().x(), c->frameGeometry().y() + c->frameGeometry().height() + 1));
523         break;
524     default:
525         break;
526     }
527     QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
528 
529     // pressing should trigger resize
530     PRESS;
531     QVERIFY(!c->isInteractiveResize());
532     QVERIFY(startMoveResizedSpy.wait());
533     QVERIFY(c->isInteractiveResize());
534 
535     RELEASE;
536     QVERIFY(!c->isInteractiveResize());
537 }
538 
testModifierClickUnrestrictedMove_data()539 void DecorationInputTest::testModifierClickUnrestrictedMove_data()
540 {
541     QTest::addColumn<int>("modifierKey");
542     QTest::addColumn<int>("mouseButton");
543     QTest::addColumn<QString>("modKey");
544     QTest::addColumn<bool>("capsLock");
545 
546     const QString alt = QStringLiteral("Alt");
547     const QString meta = QStringLiteral("Meta");
548 
549     QTest::newRow("Left Alt + Left Click")    << KEY_LEFTALT  << BTN_LEFT   << alt << false;
550     QTest::newRow("Left Alt + Right Click")   << KEY_LEFTALT  << BTN_RIGHT  << alt << false;
551     QTest::newRow("Left Alt + Middle Click")  << KEY_LEFTALT  << BTN_MIDDLE << alt << false;
552     QTest::newRow("Right Alt + Left Click")   << KEY_RIGHTALT << BTN_LEFT   << alt << false;
553     QTest::newRow("Right Alt + Right Click")  << KEY_RIGHTALT << BTN_RIGHT  << alt << false;
554     QTest::newRow("Right Alt + Middle Click") << KEY_RIGHTALT << BTN_MIDDLE << alt << false;
555     // now everything with meta
556     QTest::newRow("Left Meta + Left Click")    << KEY_LEFTMETA  << BTN_LEFT   << meta << false;
557     QTest::newRow("Left Meta + Right Click")   << KEY_LEFTMETA  << BTN_RIGHT  << meta << false;
558     QTest::newRow("Left Meta + Middle Click")  << KEY_LEFTMETA  << BTN_MIDDLE << meta << false;
559     QTest::newRow("Right Meta + Left Click")   << KEY_RIGHTMETA << BTN_LEFT   << meta << false;
560     QTest::newRow("Right Meta + Right Click")  << KEY_RIGHTMETA << BTN_RIGHT  << meta << false;
561     QTest::newRow("Right Meta + Middle Click") << KEY_RIGHTMETA << BTN_MIDDLE << meta << false;
562 
563     // and with capslock
564     QTest::newRow("Left Alt + Left Click/CapsLock")    << KEY_LEFTALT  << BTN_LEFT   << alt << true;
565     QTest::newRow("Left Alt + Right Click/CapsLock")   << KEY_LEFTALT  << BTN_RIGHT  << alt << true;
566     QTest::newRow("Left Alt + Middle Click/CapsLock")  << KEY_LEFTALT  << BTN_MIDDLE << alt << true;
567     QTest::newRow("Right Alt + Left Click/CapsLock")   << KEY_RIGHTALT << BTN_LEFT   << alt << true;
568     QTest::newRow("Right Alt + Right Click/CapsLock")  << KEY_RIGHTALT << BTN_RIGHT  << alt << true;
569     QTest::newRow("Right Alt + Middle Click/CapsLock") << KEY_RIGHTALT << BTN_MIDDLE << alt << true;
570     // now everything with meta
571     QTest::newRow("Left Meta + Left Click/CapsLock")    << KEY_LEFTMETA  << BTN_LEFT   << meta << true;
572     QTest::newRow("Left Meta + Right Click/CapsLock")   << KEY_LEFTMETA  << BTN_RIGHT  << meta << true;
573     QTest::newRow("Left Meta + Middle Click/CapsLock")  << KEY_LEFTMETA  << BTN_MIDDLE << meta << true;
574     QTest::newRow("Right Meta + Left Click/CapsLock")   << KEY_RIGHTMETA << BTN_LEFT   << meta << true;
575     QTest::newRow("Right Meta + Right Click/CapsLock")  << KEY_RIGHTMETA << BTN_RIGHT  << meta << true;
576     QTest::newRow("Right Meta + Middle Click/CapsLock") << KEY_RIGHTMETA << BTN_MIDDLE << meta << true;
577 }
578 
testModifierClickUnrestrictedMove()579 void DecorationInputTest::testModifierClickUnrestrictedMove()
580 {
581     // this test ensures that Alt+mouse button press triggers unrestricted move
582 
583     // first modify the config for this run
584     QFETCH(QString, modKey);
585     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
586     group.writeEntry("CommandAllKey", modKey);
587     group.writeEntry("CommandAll1", "Move");
588     group.writeEntry("CommandAll2", "Move");
589     group.writeEntry("CommandAll3", "Move");
590     group.sync();
591     workspace()->slotReconfigure();
592     QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier);
593     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
594     QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove);
595     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove);
596 
597     // create a window
598     AbstractClient *c = showWindow();
599     QVERIFY(c);
600     QVERIFY(c->isDecorated());
601     QVERIFY(!c->noBorder());
602     c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2));
603     // move cursor on window
604     Cursors::self()->mouse()->setPos(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2));
605 
606     // simulate modifier+click
607     quint32 timestamp = 1;
608     QFETCH(bool, capsLock);
609     if (capsLock) {
610         kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++);
611     }
612     QFETCH(int, modifierKey);
613     QFETCH(int, mouseButton);
614     kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++);
615     QVERIFY(!c->isInteractiveMove());
616     kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++);
617     QVERIFY(c->isInteractiveMove());
618     // release modifier should not change it
619     kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++);
620     QVERIFY(c->isInteractiveMove());
621     // but releasing the key should end move/resize
622     kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++);
623     QVERIFY(!c->isInteractiveMove());
624     if (capsLock) {
625         kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++);
626     }
627 }
628 
testModifierScrollOpacity_data()629 void DecorationInputTest::testModifierScrollOpacity_data()
630 {
631     QTest::addColumn<int>("modifierKey");
632     QTest::addColumn<QString>("modKey");
633     QTest::addColumn<bool>("capsLock");
634 
635     const QString alt = QStringLiteral("Alt");
636     const QString meta = QStringLiteral("Meta");
637 
638     QTest::newRow("Left Alt")   << KEY_LEFTALT  << alt << false;
639     QTest::newRow("Right Alt")  << KEY_RIGHTALT << alt << false;
640     QTest::newRow("Left Meta")  << KEY_LEFTMETA  << meta << false;
641     QTest::newRow("Right Meta") << KEY_RIGHTMETA << meta << false;
642     QTest::newRow("Left Alt/CapsLock")   << KEY_LEFTALT  << alt << true;
643     QTest::newRow("Right Alt/CapsLock")  << KEY_RIGHTALT << alt << true;
644     QTest::newRow("Left Meta/CapsLock")  << KEY_LEFTMETA  << meta << true;
645     QTest::newRow("Right Meta/CapsLock") << KEY_RIGHTMETA << meta << true;
646 }
647 
testModifierScrollOpacity()648 void DecorationInputTest::testModifierScrollOpacity()
649 {
650     // this test verifies that mod+wheel performs a window operation
651 
652     // first modify the config for this run
653     QFETCH(QString, modKey);
654     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
655     group.writeEntry("CommandAllKey", modKey);
656     group.writeEntry("CommandAllWheel", "change opacity");
657     group.sync();
658     workspace()->slotReconfigure();
659 
660     AbstractClient *c = showWindow();
661     QVERIFY(c);
662     QVERIFY(c->isDecorated());
663     QVERIFY(!c->noBorder());
664     c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2));
665     // move cursor on window
666     Cursors::self()->mouse()->setPos(QPoint(c->frameGeometry().center().x(), c->y() + c->clientPos().y() / 2));
667     // set the opacity to 0.5
668     c->setOpacity(0.5);
669     QCOMPARE(c->opacity(), 0.5);
670 
671     // simulate modifier+wheel
672     quint32 timestamp = 1;
673     QFETCH(bool, capsLock);
674     if (capsLock) {
675         kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++);
676     }
677     QFETCH(int, modifierKey);
678     kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++);
679     kwinApp()->platform()->pointerAxisVertical(-5, timestamp++);
680     QCOMPARE(c->opacity(), 0.6);
681     kwinApp()->platform()->pointerAxisVertical(5, timestamp++);
682     QCOMPARE(c->opacity(), 0.5);
683     kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++);
684     if (capsLock) {
685         kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++);
686     }
687 }
688 
689 class EventHelper : public QObject
690 {
691     Q_OBJECT
692 public:
EventHelper()693     EventHelper() : QObject() {}
694     ~EventHelper() override = default;
695 
eventFilter(QObject * watched,QEvent * event)696     bool eventFilter(QObject *watched, QEvent *event) override
697     {
698         Q_UNUSED(watched)
699         if (event->type() == QEvent::HoverMove) {
700             Q_EMIT hoverMove();
701         } else if (event->type() == QEvent::HoverLeave) {
702             Q_EMIT hoverLeave();
703         }
704         return false;
705     }
706 
707 Q_SIGNALS:
708     void hoverMove();
709     void hoverLeave();
710 };
711 
testTouchEvents()712 void DecorationInputTest::testTouchEvents()
713 {
714     // this test verifies that the decoration gets a hover leave event on touch release
715     // see BUG 386231
716     AbstractClient *c = showWindow();
717     QVERIFY(c);
718     QVERIFY(c->isDecorated());
719     QVERIFY(!c->noBorder());
720 
721     EventHelper helper;
722     c->decoration()->installEventFilter(&helper);
723     QSignalSpy hoverMoveSpy(&helper, &EventHelper::hoverMove);
724     QVERIFY(hoverMoveSpy.isValid());
725     QSignalSpy hoverLeaveSpy(&helper, &EventHelper::hoverLeave);
726     QVERIFY(hoverLeaveSpy.isValid());
727 
728     quint32 timestamp = 1;
729     const QPoint tapPoint(c->frameGeometry().center().x(), c->clientPos().y() / 2);
730 
731     QVERIFY(!input()->touch()->decoration());
732     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
733     QVERIFY(input()->touch()->decoration());
734     QCOMPARE(input()->touch()->decoration()->decoration(), c->decoration());
735     QCOMPARE(hoverMoveSpy.count(), 1);
736     QCOMPARE(hoverLeaveSpy.count(), 0);
737     kwinApp()->platform()->touchUp(0, timestamp++);
738     QCOMPARE(hoverMoveSpy.count(), 1);
739     QCOMPARE(hoverLeaveSpy.count(), 1);
740 
741     QCOMPARE(c->isInteractiveMove(), false);
742 
743     // let's check that a hover motion is sent if the pointer is on deco, when touch release
744     Cursors::self()->mouse()->setPos(tapPoint);
745     QCOMPARE(hoverMoveSpy.count(), 2);
746     kwinApp()->platform()->touchDown(0, tapPoint, timestamp++);
747     QCOMPARE(hoverMoveSpy.count(), 3);
748     QCOMPARE(hoverLeaveSpy.count(), 1);
749     kwinApp()->platform()->touchUp(0, timestamp++);
750     QCOMPARE(hoverMoveSpy.count(), 3);
751     QCOMPARE(hoverLeaveSpy.count(), 2);
752 }
753 
testTooltipDoesntEatKeyEvents()754 void DecorationInputTest::testTooltipDoesntEatKeyEvents()
755 {
756     // this test verifies that a tooltip on the decoration does not steal key events
757     // BUG: 393253
758 
759     // first create a keyboard
760     auto keyboard = Test::waylandSeat()->createKeyboard(Test::waylandSeat());
761     QVERIFY(keyboard);
762     QSignalSpy enteredSpy(keyboard, &KWayland::Client::Keyboard::entered);
763     QVERIFY(enteredSpy.isValid());
764 
765     AbstractClient *c = showWindow();
766     QVERIFY(c);
767     QVERIFY(c->isDecorated());
768     QVERIFY(!c->noBorder());
769     QVERIFY(enteredSpy.wait());
770 
771     QSignalSpy keyEvent(keyboard, &KWayland::Client::Keyboard::keyChanged);
772     QVERIFY(keyEvent.isValid());
773 
774     QSignalSpy clientAddedSpy(workspace(), &Workspace::internalClientAdded);
775     QVERIFY(clientAddedSpy.isValid());
776     c->decoratedClient()->requestShowToolTip(QStringLiteral("test"));
777     // now we should get an internal window
778     QVERIFY(clientAddedSpy.wait());
779     InternalClient *internal = clientAddedSpy.first().first().value<InternalClient *>();
780     QVERIFY(internal->isInternal());
781     QVERIFY(internal->internalWindow()->flags().testFlag(Qt::ToolTip));
782 
783     // now send a key
784     quint32 timestamp = 0;
785     kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++);
786     QVERIFY(keyEvent.wait());
787     kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++);
788     QVERIFY(keyEvent.wait());
789 
790     c->decoratedClient()->requestHideToolTip();
791     Test::waitForWindowDestroyed(internal);
792 }
793 
794 }
795 
796 WAYLANDTEST_MAIN(KWin::DecorationInputTest)
797 #include "decoration_input_test.moc"
798