diff --git a/autotests/integration/decoration_input_test.cpp b/autotests/integration/decoration_input_test.cpp index 34eb19090..68b0e691a 100644 --- a/autotests/integration/decoration_input_test.cpp +++ b/autotests/integration/decoration_input_test.cpp @@ -1,895 +1,895 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "platform.h" #include "abstract_client.h" #include "cursor.h" #include "pointer_input.h" #include "touch_input.h" #include "screenedge.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "shell_client.h" #include #include "decorations/decoratedclient.h" #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(Qt::WindowFrameSection) namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_decoration_input-0"); class DecorationInputTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testAxis_data(); void testAxis(); void testDoubleClick_data(); void testDoubleClick(); void testDoubleTap_data(); void testDoubleTap(); void testHover_data(); void testHover(); void testPressToMove_data(); void testPressToMove(); void testTapToMove_data(); void testTapToMove(); void testResizeOutsideWindow_data(); void testResizeOutsideWindow(); void testModifierClickUnrestrictedMove_data(); void testModifierClickUnrestrictedMove(); void testModifierScrollOpacity_data(); void testModifierScrollOpacity(); void testTouchEvents_data(); void testTouchEvents(); void testTooltipDoesntEatKeyEvents_data(); void testTooltipDoesntEatKeyEvents(); private: AbstractClient *showWindow(Test::ShellSurfaceType type); }; #define MOTION(target) \ kwinApp()->platform()->pointerMotion(target, timestamp++) #define PRESS \ kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++) #define RELEASE \ kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++) AbstractClient *DecorationInputTest::showWindow(Test::ShellSurfaceType type) { using namespace KWayland::Client; #define VERIFY(statement) \ if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\ return nullptr; #define COMPARE(actual, expected) \ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ return nullptr; Surface *surface = Test::createSurface(Test::waylandCompositor()); VERIFY(surface); auto shellSurface = Test::createShellSurface(type, surface, surface); VERIFY(shellSurface); auto deco = Test::waylandServerSideDecoration()->create(surface, surface); QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged); VERIFY(decoSpy.isValid()); VERIFY(decoSpy.wait()); deco->requestMode(ServerSideDecoration::Mode::Server); VERIFY(decoSpy.wait()); COMPARE(deco->mode(), ServerSideDecoration::Mode::Server); // let's render auto c = Test::renderAndWaitForShown(surface, QSize(500, 50), Qt::blue); VERIFY(c); COMPARE(workspace()->activeClient(), c); #undef VERIFY #undef COMPARE return c; } void DecorationInputTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); // change some options KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group(QStringLiteral("MouseBindings")).writeEntry("CommandTitlebarWheel", QStringLiteral("above/below")); config->group(QStringLiteral("Windows")).writeEntry("TitlebarDoubleClickCommand", QStringLiteral("OnAllDesktops")); config->group(QStringLiteral("Desktops")).writeEntry("Number", 2); config->sync(); kwinApp()->setConfig(config); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); setenv("QT_QPA_PLATFORM", "wayland", true); waylandServer()->initWorkspace(); } void DecorationInputTest::init() { using namespace KWayland::Client; QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration)); QVERIFY(Test::waitForWaylandPointer()); screens()->setCurrent(0); Cursor::setPos(QPoint(640, 512)); } void DecorationInputTest::cleanup() { Test::destroyWaylandConnection(); } void DecorationInputTest::testAxis_data() { QTest::addColumn("decoPoint"); QTest::addColumn("expectedSection"); QTest::addColumn("type"); QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topLeft|xdgv5") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("top|xdgv5") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topRight|xdgv5") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topLeft|xdgv6") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("top|xdgv6") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("topRight|xdgv6") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; } void DecorationInputTest::testAxis() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); QCOMPARE(c->titlebarPosition(), AbstractClient::PositionTop); QVERIFY(!c->keepAbove()); QVERIFY(!c->keepBelow()); quint32 timestamp = 1; MOTION(QPoint(c->geometry().center().x(), c->clientPos().y() / 2)); QVERIFY(!input()->pointer()->decoration().isNull()); QCOMPARE(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), Qt::TitleBarArea); // TODO: mouse wheel direction looks wrong to me // simulate wheel kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); QVERIFY(c->keepBelow()); QVERIFY(!c->keepAbove()); kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++); QVERIFY(!c->keepBelow()); QVERIFY(!c->keepAbove()); kwinApp()->platform()->pointerAxisVertical(-5.0, timestamp++); QVERIFY(!c->keepBelow()); QVERIFY(c->keepAbove()); // test top most deco pixel, BUG: 362860 c->move(0, 0); QFETCH(QPoint, decoPoint); MOTION(decoPoint); QVERIFY(!input()->pointer()->decoration().isNull()); QCOMPARE(input()->pointer()->decoration()->client(), c); QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); QVERIFY(!c->keepBelow()); QVERIFY(!c->keepAbove()); } void DecorationInputTest::testDoubleClick_data() { QTest::addColumn("decoPoint"); QTest::addColumn("expectedSection"); QTest::addColumn("type"); QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topLeft|xdgv5") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("top|xdgv5") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topRight|xdgv5") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topLeft|xdgv6") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("top|xdgv6") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("topRight|xdgv6") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; } void KWin::DecorationInputTest::testDoubleClick() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); QVERIFY(!c->isOnAllDesktops()); quint32 timestamp = 1; MOTION(QPoint(c->geometry().center().x(), c->clientPos().y() / 2)); // double click PRESS; RELEASE; PRESS; RELEASE; QVERIFY(c->isOnAllDesktops()); // double click again PRESS; RELEASE; QVERIFY(c->isOnAllDesktops()); PRESS; RELEASE; QVERIFY(!c->isOnAllDesktops()); // test top most deco pixel, BUG: 362860 c->move(0, 0); QFETCH(QPoint, decoPoint); MOTION(decoPoint); QVERIFY(!input()->pointer()->decoration().isNull()); QCOMPARE(input()->pointer()->decoration()->client(), c); QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); // double click PRESS; RELEASE; QVERIFY(!c->isOnAllDesktops()); PRESS; RELEASE; QVERIFY(c->isOnAllDesktops()); } void DecorationInputTest::testDoubleTap_data() { QTest::addColumn("decoPoint"); QTest::addColumn("expectedSection"); QTest::addColumn("type"); QTest::newRow("topLeft") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; QTest::newRow("top") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topRight") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; QTest::newRow("topLeft|xdgv5") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("top|xdgv5") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topRight|xdgv5") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("topLeft|xdgv6") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("top|xdgv6") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("topRight|xdgv6") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; } void KWin::DecorationInputTest::testDoubleTap() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); QVERIFY(!c->isOnAllDesktops()); quint32 timestamp = 1; const QPoint tapPoint(c->geometry().center().x(), c->clientPos().y() / 2); // double tap kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(c->isOnAllDesktops()); // double tap again kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(c->isOnAllDesktops()); kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(!c->isOnAllDesktops()); // test top most deco pixel, BUG: 362860 // // Not directly at (0, 0), otherwise ScreenEdgeInputFilter catches // event before DecorationEventFilter. c->move(10, 10); QFETCH(QPoint, decoPoint); // double click kwinApp()->platform()->touchDown(0, decoPoint, timestamp++); QVERIFY(!input()->touch()->decoration().isNull()); QCOMPARE(input()->touch()->decoration()->client(), c); QTEST(input()->touch()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(!c->isOnAllDesktops()); kwinApp()->platform()->touchDown(0, decoPoint, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(c->isOnAllDesktops()); } void DecorationInputTest::testHover_data() { QTest::addColumn("type"); QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; } void DecorationInputTest::testHover() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); // our left border is moved out of the visible area, so move the window to a better place c->move(QPoint(20, 0)); quint32 timestamp = 1; MOTION(QPoint(c->geometry().center().x(), c->clientPos().y() / 2)); QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); MOTION(QPoint(c->geometry().x(), 0)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthWest)); MOTION(QPoint(c->geometry().x() + c->geometry().width() / 2, 0)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorth)); MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, 0)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthEast)); MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, c->height() / 2)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeEast)); MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, c->height() - 1)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthEast)); MOTION(QPoint(c->geometry().x() + c->geometry().width() / 2, c->height() - 1)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouth)); MOTION(QPoint(c->geometry().x(), c->height() - 1)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthWest)); MOTION(QPoint(c->geometry().x(), c->height() / 2)); QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeWest)); MOTION(c->geometry().center()); QEXPECT_FAIL("", "Cursor not set back on leave", Continue); QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); } void DecorationInputTest::testPressToMove_data() { QTest::addColumn("offset"); QTest::addColumn("offset2"); QTest::addColumn("offset3"); QTest::addColumn("type"); QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::WlShell; QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::WlShell; QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::WlShell; QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::WlShell; QTest::newRow("To right|xdgv5") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To left|xdgv5") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To bottom|xdgv5") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To top|xdgv5") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To right|xdgv6") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To left|xdgv6") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To bottom|xdgv6") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To top|xdgv6") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV6; } void DecorationInputTest::testPressToMove() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); quint32 timestamp = 1; MOTION(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2)); QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); PRESS; QVERIFY(!c->isMove()); QFETCH(QPoint, offset); MOTION(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset); const QPoint oldPos = c->pos(); QVERIFY(c->isMove()); QCOMPARE(startMoveResizedSpy.count(), 1); RELEASE; QTRY_VERIFY(!c->isMove()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); QCOMPARE(c->pos(), oldPos + offset); // again PRESS; QVERIFY(!c->isMove()); QFETCH(QPoint, offset2); MOTION(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2); QVERIFY(c->isMove()); QCOMPARE(startMoveResizedSpy.count(), 2); QFETCH(QPoint, offset3); MOTION(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3); RELEASE; QTRY_VERIFY(!c->isMove()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2); // TODO: the offset should also be included QCOMPARE(c->pos(), oldPos + offset2 + offset3); } void DecorationInputTest::testTapToMove_data() { QTest::addColumn("offset"); QTest::addColumn("offset2"); QTest::addColumn("offset3"); QTest::addColumn("type"); QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::WlShell; QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::WlShell; QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::WlShell; QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::WlShell; QTest::newRow("To right|xdgv5") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To left|xdgv5") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To bottom|xdgv5") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To top|xdgv5") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("To right|xdgv6") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To left|xdgv6") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To bottom|xdgv6") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV6; QTest::newRow("To top|xdgv6") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV6; } void DecorationInputTest::testTapToMove() { QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); quint32 timestamp = 1; QPoint p = QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2); kwinApp()->platform()->touchDown(0, p, timestamp++); QVERIFY(!c->isMove()); QFETCH(QPoint, offset); QCOMPARE(input()->touch()->decorationPressId(), 0); kwinApp()->platform()->touchMotion(0, p + offset, timestamp++); const QPoint oldPos = c->pos(); QVERIFY(c->isMove()); QCOMPARE(startMoveResizedSpy.count(), 1); kwinApp()->platform()->touchUp(0, timestamp++); QTRY_VERIFY(!c->isMove()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); QCOMPARE(c->pos(), oldPos + offset); // again kwinApp()->platform()->touchDown(1, p + offset, timestamp++); QCOMPARE(input()->touch()->decorationPressId(), 1); QVERIFY(!c->isMove()); QFETCH(QPoint, offset2); kwinApp()->platform()->touchMotion(1, QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2, timestamp++); QVERIFY(c->isMove()); QCOMPARE(startMoveResizedSpy.count(), 2); QFETCH(QPoint, offset3); kwinApp()->platform()->touchMotion(1, QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset3, timestamp++); kwinApp()->platform()->touchUp(1, timestamp++); QTRY_VERIFY(!c->isMove()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 2); // TODO: the offset should also be included QCOMPARE(c->pos(), oldPos + offset2 + offset3); } void DecorationInputTest::testResizeOutsideWindow_data() { QTest::addColumn("type"); QTest::addColumn("edge"); QTest::addColumn("expectedCursor"); QTest::newRow("wlShell - left") << Test::ShellSurfaceType::WlShell << Qt::LeftEdge << Qt::SizeHorCursor; QTest::newRow("xdgShellV5 - left") << Test::ShellSurfaceType::XdgShellV5 << Qt::LeftEdge << Qt::SizeHorCursor; QTest::newRow("xdgShellV6 - left") << Test::ShellSurfaceType::XdgShellV6 << Qt::LeftEdge << Qt::SizeHorCursor; QTest::newRow("wlShell - right") << Test::ShellSurfaceType::WlShell << Qt::RightEdge << Qt::SizeHorCursor; QTest::newRow("xdgShellV5 - right") << Test::ShellSurfaceType::XdgShellV5 << Qt::RightEdge << Qt::SizeHorCursor; QTest::newRow("xdgShellV6 - right") << Test::ShellSurfaceType::XdgShellV6 << Qt::RightEdge << Qt::SizeHorCursor; QTest::newRow("wlShell - bottom") << Test::ShellSurfaceType::WlShell << Qt::BottomEdge << Qt::SizeVerCursor; QTest::newRow("xdgShellV5 - bottom") << Test::ShellSurfaceType::XdgShellV5 << Qt::BottomEdge << Qt::SizeVerCursor; QTest::newRow("xdgShellV6 - bottom") << Test::ShellSurfaceType::XdgShellV6 << Qt::BottomEdge << Qt::SizeVerCursor; } void DecorationInputTest::testResizeOutsideWindow() { // this test verifies that one can resize the window outside the decoration with NoSideBorder // first adjust config kwinApp()->config()->group("org.kde.kdecoration2").writeEntry("BorderSize", QStringLiteral("None")); kwinApp()->config()->sync(); workspace()->slotReconfigure(); // now create window QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); QVERIFY(c->geometry() != c->inputGeometry()); QVERIFY(c->inputGeometry().contains(c->geometry())); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); // go to border quint32 timestamp = 1; QFETCH(Qt::Edge, edge); switch (edge) { case Qt::LeftEdge: MOTION(QPoint(c->geometry().x() -1, c->geometry().center().y())); break; case Qt::RightEdge: MOTION(QPoint(c->geometry().x() + c->geometry().width() +1, c->geometry().center().y())); break; case Qt::BottomEdge: MOTION(QPoint(c->geometry().center().x(), c->geometry().y() + c->geometry().height() + 1)); break; default: break; } QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); // pressing should trigger resize PRESS; QVERIFY(!c->isResize()); QVERIFY(startMoveResizedSpy.wait()); QVERIFY(c->isResize()); RELEASE; QVERIFY(!c->isResize()); } void DecorationInputTest::testModifierClickUnrestrictedMove_data() { QTest::addColumn("modifierKey"); QTest::addColumn("mouseButton"); QTest::addColumn("modKey"); QTest::addColumn("capsLock"); QTest::addColumn("surfaceType"); const QString alt = QStringLiteral("Alt"); const QString meta = QStringLiteral("Meta"); const QVector> surfaceTypes{ {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, }; for (const auto &type: surfaceTypes) { QTest::newRow("Left Alt + Left Click" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << false << type.first; QTest::newRow("Left Alt + Right Click" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << false << type.first; QTest::newRow("Left Alt + Middle Click" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << false << type.first; QTest::newRow("Right Alt + Left Click" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << false << type.first; QTest::newRow("Right Alt + Right Click" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << false << type.first; QTest::newRow("Right Alt + Middle Click" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << false << type.first; // now everything with meta QTest::newRow("Left Meta + Left Click" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << false << type.first; QTest::newRow("Left Meta + Right Click" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << false << type.first; QTest::newRow("Left Meta + Middle Click" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << false << type.first; QTest::newRow("Right Meta + Left Click" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << false << type.first; QTest::newRow("Right Meta + Right Click" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << false << type.first; QTest::newRow("Right Meta + Middle Click" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << false << type.first; // and with capslock QTest::newRow("Left Alt + Left Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << true << type.first; QTest::newRow("Left Alt + Right Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << true << type.first; QTest::newRow("Left Alt + Middle Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << true << type.first; QTest::newRow("Right Alt + Left Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << true << type.first; QTest::newRow("Right Alt + Right Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << true << type.first; QTest::newRow("Right Alt + Middle Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << true << type.first; // now everything with meta QTest::newRow("Left Meta + Left Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << true << type.first; QTest::newRow("Left Meta + Right Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << true << type.first; QTest::newRow("Left Meta + Middle Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << true << type.first; QTest::newRow("Right Meta + Left Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << true << type.first; QTest::newRow("Right Meta + Right Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << true << type.first; QTest::newRow("Right Meta + Middle Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << true << type.first; } } void DecorationInputTest::testModifierClickUnrestrictedMove() { // this test ensures that Alt+mouse button press triggers unrestricted move // first modify the config for this run QFETCH(QString, modKey); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", modKey); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); // create a window QFETCH(Test::ShellSurfaceType, surfaceType); AbstractClient *c = showWindow(surfaceType); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); // move cursor on window Cursor::setPos(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2)); // simulate modifier+click quint32 timestamp = 1; QFETCH(bool, capsLock); if (capsLock) { kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); } QFETCH(int, modifierKey); QFETCH(int, mouseButton); kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); QVERIFY(!c->isMove()); kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++); QVERIFY(c->isMove()); // release modifier should not change it kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); QVERIFY(c->isMove()); // but releasing the key should end move/resize kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++); QVERIFY(!c->isMove()); if (capsLock) { kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); } } void DecorationInputTest::testModifierScrollOpacity_data() { QTest::addColumn("modifierKey"); QTest::addColumn("modKey"); QTest::addColumn("capsLock"); QTest::addColumn("surfaceType"); const QString alt = QStringLiteral("Alt"); const QString meta = QStringLiteral("Meta"); const QVector> surfaceTypes{ {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, }; for (const auto &type: surfaceTypes) { QTest::newRow("Left Alt" + type.second) << KEY_LEFTALT << alt << false << type.first; QTest::newRow("Right Alt" + type.second) << KEY_RIGHTALT << alt << false << type.first; QTest::newRow("Left Meta" + type.second) << KEY_LEFTMETA << meta << false << type.first; QTest::newRow("Right Meta" + type.second) << KEY_RIGHTMETA << meta << false << type.first; QTest::newRow("Left Alt/CapsLock" + type.second) << KEY_LEFTALT << alt << true << type.first; QTest::newRow("Right Alt/CapsLock" + type.second) << KEY_RIGHTALT << alt << true << type.first; QTest::newRow("Left Meta/CapsLock" + type.second) << KEY_LEFTMETA << meta << true << type.first; QTest::newRow("Right Meta/CapsLock" + type.second) << KEY_RIGHTMETA << meta << true << type.first; } } void DecorationInputTest::testModifierScrollOpacity() { // this test verifies that mod+wheel performs a window operation // first modify the config for this run QFETCH(QString, modKey); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", modKey); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); QFETCH(Test::ShellSurfaceType, surfaceType); AbstractClient *c = showWindow(surfaceType); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); // move cursor on window Cursor::setPos(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2)); // set the opacity to 0.5 c->setOpacity(0.5); QCOMPARE(c->opacity(), 0.5); // simulate modifier+wheel quint32 timestamp = 1; QFETCH(bool, capsLock); if (capsLock) { kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); } QFETCH(int, modifierKey); kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(c->opacity(), 0.6); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(c->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); if (capsLock) { kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); } } void DecorationInputTest::testTouchEvents_data() { QTest::addColumn("type"); QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; } class EventHelper : public QObject { Q_OBJECT public: EventHelper() : QObject() {} ~EventHelper() override = default; bool eventFilter(QObject *watched, QEvent *event) override { Q_UNUSED(watched) if (event->type() == QEvent::HoverMove) { emit hoverMove(); } else if (event->type() == QEvent::HoverLeave) { emit hoverLeave(); } return false; } Q_SIGNALS: void hoverMove(); void hoverLeave(); }; void DecorationInputTest::testTouchEvents() { // this test verifies that the decoration gets a hover leave event on touch release // see BUG 386231 QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); EventHelper helper; c->decoration()->installEventFilter(&helper); QSignalSpy hoverMoveSpy(&helper, &EventHelper::hoverMove); QVERIFY(hoverMoveSpy.isValid()); QSignalSpy hoverLeaveSpy(&helper, &EventHelper::hoverLeave); QVERIFY(hoverLeaveSpy.isValid()); quint32 timestamp = 1; const QPoint tapPoint(c->geometry().center().x(), c->clientPos().y() / 2); QVERIFY(!input()->touch()->decoration()); kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); QVERIFY(input()->touch()->decoration()); QCOMPARE(input()->touch()->decoration()->decoration(), c->decoration()); QCOMPARE(hoverMoveSpy.count(), 1); QCOMPARE(hoverLeaveSpy.count(), 0); kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(hoverMoveSpy.count(), 1); QCOMPARE(hoverLeaveSpy.count(), 1); QCOMPARE(c->isMove(), false); // let's check that a hover motion is sent if the pointer is on deco, when touch release Cursor::setPos(tapPoint); QCOMPARE(hoverMoveSpy.count(), 2); kwinApp()->platform()->touchDown(0, tapPoint, timestamp++); QCOMPARE(hoverMoveSpy.count(), 3); QCOMPARE(hoverLeaveSpy.count(), 1); kwinApp()->platform()->touchUp(0, timestamp++); - QCOMPARE(hoverMoveSpy.count(), 4); - QCOMPARE(hoverLeaveSpy.count(), 1); + QCOMPARE(hoverMoveSpy.count(), 3); + QCOMPARE(hoverLeaveSpy.count(), 2); } void DecorationInputTest::testTooltipDoesntEatKeyEvents_data() { QTest::addColumn("type"); QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; } void DecorationInputTest::testTooltipDoesntEatKeyEvents() { // this test verifies that a tooltip on the decoration does not steal key events // BUG: 393253 // first create a keyboard auto keyboard = Test::waylandSeat()->createKeyboard(Test::waylandSeat()); QVERIFY(keyboard); QSignalSpy enteredSpy(keyboard, &KWayland::Client::Keyboard::entered); QVERIFY(enteredSpy.isValid()); QFETCH(Test::ShellSurfaceType, type); AbstractClient *c = showWindow(type); QVERIFY(c); QVERIFY(c->isDecorated()); QVERIFY(!c->noBorder()); QTRY_COMPARE(enteredSpy.count(), 1); QSignalSpy keyEvent(keyboard, &KWayland::Client::Keyboard::keyChanged); QVERIFY(keyEvent.isValid()); QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); c->decoratedClient()->requestShowToolTip(QStringLiteral("test")); // now we should get an internal window QVERIFY(clientAddedSpy.wait()); ShellClient *internal = clientAddedSpy.first().first().value(); QVERIFY(internal->isInternal()); QVERIFY(internal->internalWindow()->flags().testFlag(Qt::ToolTip)); // now send a key quint32 timestamp = 0; kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QVERIFY(keyEvent.wait()); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QVERIFY(keyEvent.wait()); c->decoratedClient()->requestHideToolTip(); Test::waitForWindowDestroyed(internal); } } WAYLANDTEST_MAIN(KWin::DecorationInputTest) #include "decoration_input_test.moc" diff --git a/autotests/integration/internal_window.cpp b/autotests/integration/internal_window.cpp index a1fb1a6c4..8936b9189 100644 --- a/autotests/integration/internal_window.cpp +++ b/autotests/integration/internal_window.cpp @@ -1,677 +1,677 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "platform.h" #include "cursor.h" #include "shell_client.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include using namespace KWayland::Client; namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_internal_window-0"); class InternalWindowTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testEnterLeave(); void testPointerPressRelease(); void testPointerAxis(); void testKeyboard_data(); void testKeyboard(); void testKeyboardShowWithoutActivating(); void testKeyboardTriggersLeave(); void testTouch(); void testOpacity(); void testMove(); void testSkipCloseAnimation_data(); void testSkipCloseAnimation(); void testModifierClickUnrestrictedMove(); void testModifierScroll(); }; class HelperWindow : public QRasterWindow { Q_OBJECT public: HelperWindow(); ~HelperWindow(); QPoint latestGlobalMousePos() const { return m_latestGlobalMousePos; } Qt::MouseButtons pressedButtons() const { return m_pressedButtons; } Q_SIGNALS: void entered(); void left(); void mouseMoved(const QPoint &global); void mousePressed(); void mouseReleased(); void wheel(); void keyPressed(); void keyReleased(); protected: void paintEvent(QPaintEvent *event) override; bool event(QEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private: QPoint m_latestGlobalMousePos; Qt::MouseButtons m_pressedButtons = Qt::MouseButtons(); }; HelperWindow::HelperWindow() : QRasterWindow(nullptr) { setFlags(Qt::FramelessWindowHint); } HelperWindow::~HelperWindow() = default; void HelperWindow::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter p(this); p.fillRect(0, 0, width(), height(), Qt::red); } bool HelperWindow::event(QEvent *event) { if (event->type() == QEvent::Enter) { emit entered(); } if (event->type() == QEvent::Leave) { emit left(); } return QRasterWindow::event(event); } void HelperWindow::mouseMoveEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); emit mouseMoved(event->globalPos()); } void HelperWindow::mousePressEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); m_pressedButtons = event->buttons(); emit mousePressed(); } void HelperWindow::mouseReleaseEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); m_pressedButtons = event->buttons(); emit mouseReleased(); } void HelperWindow::wheelEvent(QWheelEvent *event) { Q_UNUSED(event) emit wheel(); } void HelperWindow::keyPressEvent(QKeyEvent *event) { Q_UNUSED(event) emit keyPressed(); } void HelperWindow::keyReleaseEvent(QKeyEvent *event) { Q_UNUSED(event) emit keyReleased(); } void InternalWindowTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); waylandServer()->initWorkspace(); } void InternalWindowTest::init() { Cursor::setPos(QPoint(1280, 512)); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandKeyboard()); } void InternalWindowTest::cleanup() { Test::destroyWaylandConnection(); } void InternalWindowTest::testEnterLeave() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; QVERIFY(!workspace()->findToplevel(nullptr)); QVERIFY(!workspace()->findToplevel(&win)); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); QVERIFY(!workspace()->activeClient()); ShellClient *c = clientAddedSpy.first().first().value(); QVERIFY(c->isInternal()); QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QVERIFY(!c->isDecorated()); QCOMPARE(workspace()->findToplevel(&win), c); QCOMPARE(c->geometry(), QRect(0, 0, 100, 100)); QVERIFY(c->isShown(false)); QVERIFY(workspace()->xStackingOrder().contains(c)); QSignalSpy enterSpy(&win, &HelperWindow::entered); QVERIFY(enterSpy.isValid()); QSignalSpy leaveSpy(&win, &HelperWindow::left); QVERIFY(leaveSpy.isValid()); QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); QVERIFY(moveSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); - QTRY_COMPARE(enterSpy.count(), 1); + QTRY_COMPARE(moveSpy.count(), 1); kwinApp()->platform()->pointerMotion(QPoint(60, 50), timestamp++); - QTRY_COMPARE(moveSpy.count(), 1); - QCOMPARE(moveSpy.first().first().toPoint(), QPoint(60, 50)); + QTRY_COMPARE(moveSpy.count(), 2); + QCOMPARE(moveSpy[1].first().toPoint(), QPoint(60, 50)); kwinApp()->platform()->pointerMotion(QPoint(101, 50), timestamp++); QTRY_COMPARE(leaveSpy.count(), 1); // set a mask on the window win.setMask(QRegion(10, 20, 30, 40)); // outside the mask we should not get an enter kwinApp()->platform()->pointerMotion(QPoint(5, 5), timestamp++); QVERIFY(!enterSpy.wait(100)); QCOMPARE(enterSpy.count(), 1); // inside the mask we should still get an enter kwinApp()->platform()->pointerMotion(QPoint(25, 27), timestamp++); QTRY_COMPARE(enterSpy.count(), 2); // hide the window, which should be removed from the stacking order win.hide(); QTRY_VERIFY(!c->isShown(false)); QVERIFY(!workspace()->xStackingOrder().contains(c)); // show again win.show(); QTRY_VERIFY(c->isShown(false)); QVERIFY(workspace()->xStackingOrder().contains(c)); } void InternalWindowTest::testPointerPressRelease() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QTRY_COMPARE(pressSpy.count(), 1); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); } void InternalWindowTest::testPointerAxis() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy wheelSpy(&win, &HelperWindow::wheel); QVERIFY(wheelSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); QTRY_COMPARE(wheelSpy.count(), 1); kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); QTRY_COMPARE(wheelSpy.count(), 2); } void InternalWindowTest::testKeyboard_data() { QTest::addColumn("cursorPos"); QTest::newRow("on Window") << QPoint(50, 50); QTest::newRow("outside Window") << QPoint(250, 250); } void InternalWindowTest::testKeyboard() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); quint32 timestamp = 1; QFETCH(QPoint, cursorPos); kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QTRY_COMPARE(pressSpy.count(), 1); QCOMPARE(releaseSpy.count(), 0); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); QCOMPARE(pressSpy.count(), 1); } void InternalWindowTest::testKeyboardShowWithoutActivating() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setProperty("_q_showWithoutActivating", true); win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); quint32 timestamp = 1; const QPoint cursorPos = QPoint(50, 50); kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QCOMPARE(pressSpy.count(), 0); QVERIFY(!pressSpy.wait(100)); QCOMPARE(releaseSpy.count(), 0); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QCOMPARE(releaseSpy.count(), 0); QVERIFY(!releaseSpy.wait(100)); QCOMPARE(pressSpy.count(), 0); } void InternalWindowTest::testKeyboardTriggersLeave() { // this test verifies that a leave event is sent to a client when an internal window // gets a key event QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QVERIFY(!keyboard.isNull()); QVERIFY(keyboard->isValid()); QSignalSpy enteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(keyboard.data(), &Keyboard::left); QVERIFY(leftSpy.isValid()); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(Test::ShellSurfaceType::WlShell, surface.data())); // now let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isInternal()); if (enteredSpy.isEmpty()) { QVERIFY(enteredSpy.wait()); } QCOMPARE(enteredSpy.count(), 1); // create internal window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); QVERIFY(leftSpy.isEmpty()); QVERIFY(!leftSpy.wait(100)); // now let's trigger a key, which should result in a leave quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QVERIFY(leftSpy.wait()); QCOMPARE(pressSpy.count(), 1); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); // after hiding the internal window, next key press should trigger an enter win.hide(); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QVERIFY(enteredSpy.wait()); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); } void InternalWindowTest::testTouch() { // touch events for internal windows are emulated through mouse events QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); QVERIFY(releaseSpy.isValid()); QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); QVERIFY(moveSpy.isValid()); quint32 timestamp = 1; QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); kwinApp()->platform()->touchDown(0, QPointF(50, 50), timestamp++); QCOMPARE(pressSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // further touch down should not trigger kwinApp()->platform()->touchDown(1, QPointF(75, 75), timestamp++); QCOMPARE(pressSpy.count(), 1); kwinApp()->platform()->touchUp(1, timestamp++); QCOMPARE(releaseSpy.count(), 0); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // another press kwinApp()->platform()->touchDown(1, QPointF(10, 10), timestamp++); QCOMPARE(pressSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // simulate the move QCOMPARE(moveSpy.count(), 0); kwinApp()->platform()->touchMotion(0, QPointF(80, 90), timestamp++); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // move on other ID should not do anything kwinApp()->platform()->touchMotion(1, QPointF(20, 30), timestamp++); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // now up our main point kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(releaseSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); // and up the additional point kwinApp()->platform()->touchUp(1, timestamp++); QCOMPARE(releaseSpy.count(), 1); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); } void InternalWindowTest::testOpacity() { // this test verifies that opacity is properly synced from QWindow to ShellClient QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QCOMPARE(internalClient->opacity(), 0.5); QSignalSpy opacityChangedSpy(internalClient, &ShellClient::opacityChanged); QVERIFY(opacityChangedSpy.isValid()); win.setOpacity(0.75); QCOMPARE(opacityChangedSpy.count(), 1); QCOMPARE(internalClient->opacity(), 0.75); } void InternalWindowTest::testMove() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QCOMPARE(internalClient->geometry(), QRect(0, 0, 100, 100)); // normal move should be synced internalClient->move(5, 10); QCOMPARE(internalClient->geometry(), QRect(5, 10, 100, 100)); QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); // another move should also be synced internalClient->move(10, 20); QCOMPARE(internalClient->geometry(), QRect(10, 20, 100, 100)); QTRY_COMPARE(win.geometry(), QRect(10, 20, 100, 100)); // now move with a Geometry update blocker { GeometryUpdatesBlocker blocker(internalClient); internalClient->move(5, 10); // not synced! QCOMPARE(win.geometry(), QRect(10, 20, 100, 100)); } // after destroying the blocker it should be synced QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); } void InternalWindowTest::testSkipCloseAnimation_data() { QTest::addColumn("initial"); QTest::newRow("set") << true; QTest::newRow("not set") << false; } void InternalWindowTest::testSkipCloseAnimation() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); QFETCH(bool, initial); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QCOMPARE(internalClient->skipsCloseAnimation(), initial); QSignalSpy skipCloseChangedSpy(internalClient, &Toplevel::skipCloseAnimationChanged); QVERIFY(skipCloseChangedSpy.isValid()); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", !initial); QCOMPARE(skipCloseChangedSpy.count(), 1); QCOMPARE(internalClient->skipsCloseAnimation(), !initial); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); QCOMPARE(skipCloseChangedSpy.count(), 2); QCOMPARE(internalClient->skipsCloseAnimation(), initial); } void InternalWindowTest::testModifierClickUnrestrictedMove() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.setFlags(win.flags() & ~Qt::FramelessWindowHint); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isDecorated()); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), Qt::AltModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); // move cursor on window Cursor::setPos(internalClient->geometry().center()); // simulate modifier+click quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); QVERIFY(!internalClient->isMove()); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(internalClient->isMove()); // release modifier should not change it kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); QVERIFY(internalClient->isMove()); // but releasing the key should end move/resize kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(!internalClient->isMove()); } void InternalWindowTest::testModifierScroll() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.setFlags(win.flags() & ~Qt::FramelessWindowHint); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isDecorated()); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); // move cursor on window Cursor::setPos(internalClient->geometry().center()); // set the opacity to 0.5 internalClient->setOpacity(0.5); QCOMPARE(internalClient->opacity(), 0.5); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(internalClient->opacity(), 0.6); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(internalClient->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); } } WAYLANDTEST_MAIN(KWin::InternalWindowTest) #include "internal_window.moc" diff --git a/autotests/integration/pointer_constraints_test.cpp b/autotests/integration/pointer_constraints_test.cpp index 43b813c7a..c2cb02136 100644 --- a/autotests/integration/pointer_constraints_test.cpp +++ b/autotests/integration/pointer_constraints_test.cpp @@ -1,417 +1,417 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "cursor.h" #include "keyboard_input.h" #include "platform.h" #include "pointer_input.h" #include "shell_client.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; typedef std::function PointerFunc; Q_DECLARE_METATYPE(PointerFunc) static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0"); class TestPointerConstraints : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testConfinedPointer_data(); void testConfinedPointer(); void testLockedPointer_data(); void testLockedPointer(); void testCloseWindowWithLockedPointer_data(); void testCloseWindowWithLockedPointer(); }; void TestPointerConstraints::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); // set custom config which disables the OnScreenNotification KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup group = config->group("OnScreenNotification"); group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); group.sync(); kwinApp()->setConfig(config); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); waylandServer()->initWorkspace(); } void TestPointerConstraints::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints)); QVERIFY(Test::waitForWaylandPointer()); screens()->setCurrent(0); KWin::Cursor::setPos(QPoint(1280, 512)); } void TestPointerConstraints::cleanup() { Test::destroyWaylandConnection(); } void TestPointerConstraints::testConfinedPointer_data() { QTest::addColumn("type"); QTest::addColumn("positionFunction"); QTest::addColumn("xOffset"); QTest::addColumn("yOffset"); PointerFunc bottomLeft = &QRect::bottomLeft; PointerFunc bottomRight = &QRect::bottomRight; PointerFunc topRight = &QRect::topRight; PointerFunc topLeft = &QRect::topLeft; QTest::newRow("wlShell - bottomLeft") << Test::ShellSurfaceType::WlShell << bottomLeft << -1 << 1; QTest::newRow("wlShell - bottomRight") << Test::ShellSurfaceType::WlShell << bottomRight << 1 << 1; QTest::newRow("wlShell - topLeft") << Test::ShellSurfaceType::WlShell << topLeft << -1 << -1; QTest::newRow("wlShell - topRight") << Test::ShellSurfaceType::WlShell << topRight << 1 << -1; QTest::newRow("XdgShellV5 - bottomLeft") << Test::ShellSurfaceType::XdgShellV5 << bottomLeft << -1 << 1; QTest::newRow("XdgShellV5 - bottomRight") << Test::ShellSurfaceType::XdgShellV5 << bottomRight << 1 << 1; QTest::newRow("XdgShellV5 - topLeft") << Test::ShellSurfaceType::XdgShellV5 << topLeft << -1 << -1; QTest::newRow("XdgShellV5 - topRight") << Test::ShellSurfaceType::XdgShellV5 << topRight << 1 << -1; QTest::newRow("XdgShellV6 - bottomLeft") << Test::ShellSurfaceType::XdgShellV6 << bottomLeft << -1 << 1; QTest::newRow("XdgShellV6 - bottomRight") << Test::ShellSurfaceType::XdgShellV6 << bottomRight << 1 << 1; QTest::newRow("XdgShellV6 - topLeft") << Test::ShellSurfaceType::XdgShellV6 << topLeft << -1 << -1; QTest::newRow("XdgShellV6 - topRight") << Test::ShellSurfaceType::XdgShellV6 << topRight << 1 << -1; } void TestPointerConstraints::testConfinedPointer() { // this test sets up a Surface with a confined pointer // simple interaction test to verify that the pointer gets confined QScopedPointer surface(Test::createSurface()); QFETCH(Test::ShellSurfaceType, type); QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy confinedSpy(confinedPointer.data(), &ConfinedPointer::confined); QVERIFY(confinedSpy.isValid()); QSignalSpy unconfinedSpy(confinedPointer.data(), &ConfinedPointer::unconfined); QVERIFY(unconfinedSpy.isValid()); // now map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); QVERIFY(c); if (c->geometry().topLeft() == QPoint(0, 0)) { c->move(QPoint(1, 1)); } QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); // now let's confine QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursor::setPos(c->geometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(confinedSpy.wait()); // picking a position outside the window geometry should not move pointer QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged); QVERIFY(pointerPositionChangedSpy.isValid()); KWin::Cursor::setPos(QPoint(1280, 512)); QVERIFY(pointerPositionChangedSpy.isEmpty()); QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); // TODO: test relative motion QFETCH(PointerFunc, positionFunction); const QPoint position = positionFunction(c->geometry()); KWin::Cursor::setPos(position); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursor::pos(), position); // moving one to right should not be possible QFETCH(int, xOffset); KWin::Cursor::setPos(position + QPoint(xOffset, 0)); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursor::pos(), position); // moving one to bottom should not be possible QFETCH(int, yOffset); KWin::Cursor::setPos(position + QPoint(0, yOffset)); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursor::pos(), position); // modifier + click should be ignored // first ensure the settings are ok KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", QStringLiteral("Alt")); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), Qt::AltModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(!c->isMove()); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); // set the opacity to 0.5 c->setOpacity(0.5); QCOMPARE(c->opacity(), 0.5); // pointer is confined so shortcut should not work kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(c->opacity(), 0.5); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(c->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); // deactivate the client, this should unconfine workspace()->activateClient(nullptr); QVERIFY(unconfinedSpy.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // reconfine pointer (this time with persistent life time) confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy confinedSpy2(confinedPointer.data(), &ConfinedPointer::confined); QVERIFY(confinedSpy2.isValid()); QSignalSpy unconfinedSpy2(confinedPointer.data(), &ConfinedPointer::unconfined); QVERIFY(unconfinedSpy2.isValid()); // activate it again, this confines again - workspace()->activateClient(static_cast(input()->pointer()->window().data())); + workspace()->activateClient(static_cast(input()->pointer()->focus().data())); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // deactivate the client one more time with the persistent life time constraint, this should unconfine workspace()->activateClient(nullptr); QVERIFY(unconfinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // activate it again, this confines again - workspace()->activateClient(static_cast(input()->pointer()->window().data())); + workspace()->activateClient(static_cast(input()->pointer()->focus().data())); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // create a second window and move it above our constrained window QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createShellSurface(type, surface2.data())); auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(1280, 1024), Qt::blue); QVERIFY(c2); QVERIFY(unconfinedSpy2.wait()); // and unmapping the second window should confine again shellSurface2.reset(); surface2.reset(); QVERIFY(confinedSpy2.wait()); // let's set a region which results in unconfined auto r = Test::waylandCompositor()->createRegion(QRegion(2, 2, 3, 3)); confinedPointer->setRegion(r.get()); surface->commit(Surface::CommitFlag::None); QVERIFY(unconfinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // and set a full region again, that should confine confinedPointer->setRegion(nullptr); surface->commit(Surface::CommitFlag::None); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // delete pointer confine confinedPointer.reset(nullptr); Test::flushWaylandConnection(); - QSignalSpy constraintsChangedSpy(input()->pointer()->window()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged); + QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged); QVERIFY(constraintsChangedSpy.isValid()); QVERIFY(constraintsChangedSpy.wait()); // should be unconfined QCOMPARE(input()->pointer()->isConstrained(), false); // confine again confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy confinedSpy3(confinedPointer.data(), &ConfinedPointer::confined); QVERIFY(confinedSpy3.isValid()); QVERIFY(confinedSpy3.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // and now unmap shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); QCOMPARE(input()->pointer()->isConstrained(), false); } void TestPointerConstraints::testLockedPointer_data() { QTest::addColumn("type"); QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; } void TestPointerConstraints::testLockedPointer() { // this test sets up a Surface with a locked pointer // simple interaction test to verify that the pointer gets locked // the various ways to unlock are not tested as that's already verified by testConfinedPointer QScopedPointer surface(Test::createSurface()); QFETCH(Test::ShellSurfaceType, type); QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); QVERIFY(lockedSpy.isValid()); QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); QVERIFY(unlockedSpy.isValid()); // now map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); QVERIFY(c); QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); // now let's lock QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursor::setPos(c->geometry().center()); QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(lockedSpy.wait()); // try to move the pointer // TODO: add relative pointer KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); // deactivate the client, this should unlock workspace()->activateClient(nullptr); QCOMPARE(input()->pointer()->isConstrained(), false); QVERIFY(unlockedSpy.wait()); // moving cursor should be allowed again KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); QCOMPARE(KWin::Cursor::pos(), c->geometry().center() + QPoint(1, 1)); lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy lockedSpy2(lockedPointer.data(), &LockedPointer::locked); QVERIFY(lockedSpy2.isValid()); // activate the client again, this should lock again - workspace()->activateClient(static_cast(input()->pointer()->window().data())); + workspace()->activateClient(static_cast(input()->pointer()->focus().data())); QVERIFY(lockedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // try to move the pointer QCOMPARE(input()->pointer()->isConstrained(), true); KWin::Cursor::setPos(c->geometry().center()); QCOMPARE(KWin::Cursor::pos(), c->geometry().center() + QPoint(1, 1)); // delete pointer lock lockedPointer.reset(nullptr); Test::flushWaylandConnection(); - QSignalSpy constraintsChangedSpy(input()->pointer()->window()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged); + QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged); QVERIFY(constraintsChangedSpy.isValid()); QVERIFY(constraintsChangedSpy.wait()); // moving cursor should be allowed again QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursor::setPos(c->geometry().center()); QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); } void TestPointerConstraints::testCloseWindowWithLockedPointer_data() { QTest::addColumn("type"); QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; QTest::newRow("XdgShellV5") << Test::ShellSurfaceType::XdgShellV5; QTest::newRow("XdgShellV6") << Test::ShellSurfaceType::XdgShellV6; } void TestPointerConstraints::testCloseWindowWithLockedPointer() { // test case which verifies that the pointer gets unlocked when the window for it gets closed QScopedPointer surface(Test::createSurface()); QFETCH(Test::ShellSurfaceType, type); QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); QVERIFY(lockedSpy.isValid()); QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); QVERIFY(unlockedSpy.isValid()); // now map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); QVERIFY(c); QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); // now let's lock QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursor::setPos(c->geometry().center()); QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(lockedSpy.wait()); // close the window shellSurface.reset(); surface.reset(); // this should result in unlocked QVERIFY(unlockedSpy.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); } WAYLANDTEST_MAIN(TestPointerConstraints) #include "pointer_constraints_test.moc" diff --git a/autotests/integration/pointer_input.cpp b/autotests/integration/pointer_input.cpp index 1302fbf0a..dfc859ff9 100644 --- a/autotests/integration/pointer_input.cpp +++ b/autotests/integration/pointer_input.cpp @@ -1,1418 +1,1418 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "platform.h" #include "abstract_client.h" #include "cursor.h" #include "deleted.h" #include "effects.h" #include "pointer_input.h" #include "options.h" #include "screenedge.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "shell_client.h" #include #include #include #include #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_input-0"); class PointerInputTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testWarpingUpdatesFocus(); void testWarpingGeneratesPointerMotion(); void testWarpingDuringFilter(); void testUpdateFocusAfterScreenChange(); void testModifierClickUnrestrictedMove_data(); void testModifierClickUnrestrictedMove(); void testModifierClickUnrestrictedMoveGlobalShortcutsDisabled(); void testModifierScrollOpacity_data(); void testModifierScrollOpacity(); void testModifierScrollOpacityGlobalShortcutsDisabled(); void testScrollAction(); void testFocusFollowsMouse(); void testMouseActionInactiveWindow_data(); void testMouseActionInactiveWindow(); void testMouseActionActiveWindow_data(); void testMouseActionActiveWindow(); void testCursorImage(); void testEffectOverrideCursorImage(); void testPopup(); void testDecoCancelsPopup(); void testWindowUnderCursorWhileButtonPressed(); void testConfineToScreenGeometry_data(); void testConfineToScreenGeometry(); private: void render(KWayland::Client::Surface *surface, const QSize &size = QSize(100, 50)); KWayland::Client::Compositor *m_compositor = nullptr; KWayland::Client::Seat *m_seat = nullptr; KWayland::Client::Shell *m_shell = nullptr; }; void PointerInputTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); } else { // might be vanilla-dmz (e.g. Arch, FreeBSD) qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); } qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); qputenv("XKB_DEFAULT_RULES", "evdev"); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); setenv("QT_QPA_PLATFORM", "wayland", true); waylandServer()->initWorkspace(); } void PointerInputTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::Decoration)); QVERIFY(Test::waitForWaylandPointer()); m_compositor = Test::waylandCompositor(); m_shell = Test::waylandShell(); m_seat = Test::waylandSeat(); screens()->setCurrent(0); Cursor::setPos(QPoint(640, 512)); } void PointerInputTest::cleanup() { Test::destroyWaylandConnection(); } void PointerInputTest::render(KWayland::Client::Surface *surface, const QSize &size) { Test::render(surface, size, Qt::blue); Test::flushWaylandConnection(); } void PointerInputTest::testWarpingUpdatesFocus() { // this test verifies that warping the pointer creates pointer enter and leave events using namespace KWayland::Client; // create pointer and signal spy for enter and leave signals auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(pointer, &Pointer::left); QVERIFY(leftSpy.isValid()); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // currently there should not be a focused pointer surface QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); QVERIFY(!pointer->enteredSurface()); // enter Cursor::setPos(QPoint(25, 25)); QVERIFY(enteredSpy.wait()); QCOMPARE(enteredSpy.count(), 1); QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25)); // window should have focus QCOMPARE(pointer->enteredSurface(), surface); // also on the server QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); // and out again Cursor::setPos(QPoint(250, 250));; QVERIFY(leftSpy.wait()); QCOMPARE(leftSpy.count(), 1); // there should not be a focused pointer surface anymore QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); QVERIFY(!pointer->enteredSurface()); } void PointerInputTest::testWarpingGeneratesPointerMotion() { // this test verifies that warping the pointer creates pointer motion events using namespace KWayland::Client; // create pointer and signal spy for enter and motion auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy movedSpy(pointer, &Pointer::motion); QVERIFY(movedSpy.isValid()); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // enter kwinApp()->platform()->pointerMotion(QPointF(25, 25), 1); QVERIFY(enteredSpy.wait()); QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25)); // now warp Cursor::setPos(QPoint(26, 26)); QVERIFY(movedSpy.wait()); QCOMPARE(movedSpy.count(), 1); QCOMPARE(movedSpy.last().first().toPointF(), QPointF(26, 26)); } void PointerInputTest::testWarpingDuringFilter() { // this test verifies that pointer motion is handled correctly if // the pointer gets warped during processing of input events using namespace KWayland::Client; // create pointer auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy movedSpy(pointer, &Pointer::motion); QVERIFY(movedSpy.isValid()); // warp cursor into expected geometry Cursor::setPos(10, 10); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); QCOMPARE(window->pos(), QPoint(0, 0)); QVERIFY(window->geometry().contains(Cursor::pos())); // is PresentWindows effect for top left screen edge loaded QVERIFY(static_cast(effects)->isEffectLoaded("presentwindows")); QVERIFY(movedSpy.isEmpty()); quint32 timestamp = 0; kwinApp()->platform()->pointerMotion(QPoint(0, 0), timestamp++); // screen edges push back QCOMPARE(Cursor::pos(), QPoint(1, 1)); QVERIFY(movedSpy.wait()); QCOMPARE(movedSpy.count(), 2); QCOMPARE(movedSpy.at(0).first().toPoint(), QPoint(0, 0)); QCOMPARE(movedSpy.at(1).first().toPoint(), QPoint(1, 1)); } void PointerInputTest::testUpdateFocusAfterScreenChange() { // this test verifies that a pointer enter event is generated when the cursor changes to another // screen due to removal of screen using namespace KWayland::Client; // ensure cursor is on second screen Cursor::setPos(1500, 300); // create pointer and signal spy for enter and motion auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface, QSize(1280, 1024)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); QVERIFY(!window->geometry().contains(Cursor::pos())); QSignalSpy screensChangedSpy(screens(), &Screens::changed); QVERIFY(screensChangedSpy.isValid()); // now let's remove the screen containing the cursor QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 1), Q_ARG(QVector, QVector{QRect(0, 0, 1280, 1024)})); QVERIFY(screensChangedSpy.wait()); QCOMPARE(screens()->count(), 1); // this should have warped the cursor QCOMPARE(Cursor::pos(), QPoint(639, 511)); QVERIFY(window->geometry().contains(Cursor::pos())); // and we should get an enter event QVERIFY(enteredSpy.wait()); QCOMPARE(enteredSpy.count(), 1); } void PointerInputTest::testModifierClickUnrestrictedMove_data() { QTest::addColumn("modifierKey"); QTest::addColumn("mouseButton"); QTest::addColumn("modKey"); QTest::addColumn("capsLock"); const QString alt = QStringLiteral("Alt"); const QString meta = QStringLiteral("Meta"); QTest::newRow("Left Alt + Left Click") << KEY_LEFTALT << BTN_LEFT << alt << false; QTest::newRow("Left Alt + Right Click") << KEY_LEFTALT << BTN_RIGHT << alt << false; QTest::newRow("Left Alt + Middle Click") << KEY_LEFTALT << BTN_MIDDLE << alt << false; QTest::newRow("Right Alt + Left Click") << KEY_RIGHTALT << BTN_LEFT << alt << false; QTest::newRow("Right Alt + Right Click") << KEY_RIGHTALT << BTN_RIGHT << alt << false; QTest::newRow("Right Alt + Middle Click") << KEY_RIGHTALT << BTN_MIDDLE << alt << false; // now everything with meta QTest::newRow("Left Meta + Left Click") << KEY_LEFTMETA << BTN_LEFT << meta << false; QTest::newRow("Left Meta + Right Click") << KEY_LEFTMETA << BTN_RIGHT << meta << false; QTest::newRow("Left Meta + Middle Click") << KEY_LEFTMETA << BTN_MIDDLE << meta << false; QTest::newRow("Right Meta + Left Click") << KEY_RIGHTMETA << BTN_LEFT << meta << false; QTest::newRow("Right Meta + Right Click") << KEY_RIGHTMETA << BTN_RIGHT << meta << false; QTest::newRow("Right Meta + Middle Click") << KEY_RIGHTMETA << BTN_MIDDLE << meta << false; // and with capslock QTest::newRow("Left Alt + Left Click/CapsLock") << KEY_LEFTALT << BTN_LEFT << alt << true; QTest::newRow("Left Alt + Right Click/CapsLock") << KEY_LEFTALT << BTN_RIGHT << alt << true; QTest::newRow("Left Alt + Middle Click/CapsLock") << KEY_LEFTALT << BTN_MIDDLE << alt << true; QTest::newRow("Right Alt + Left Click/CapsLock") << KEY_RIGHTALT << BTN_LEFT << alt << true; QTest::newRow("Right Alt + Right Click/CapsLock") << KEY_RIGHTALT << BTN_RIGHT << alt << true; QTest::newRow("Right Alt + Middle Click/CapsLock") << KEY_RIGHTALT << BTN_MIDDLE << alt << true; // now everything with meta QTest::newRow("Left Meta + Left Click/CapsLock") << KEY_LEFTMETA << BTN_LEFT << meta << true; QTest::newRow("Left Meta + Right Click/CapsLock") << KEY_LEFTMETA << BTN_RIGHT << meta << true; QTest::newRow("Left Meta + Middle Click/CapsLock") << KEY_LEFTMETA << BTN_MIDDLE << meta << true; QTest::newRow("Right Meta + Left Click/CapsLock") << KEY_RIGHTMETA << BTN_LEFT << meta << true; QTest::newRow("Right Meta + Right Click/CapsLock") << KEY_RIGHTMETA << BTN_RIGHT << meta << true; QTest::newRow("Right Meta + Middle Click/CapsLock") << KEY_RIGHTMETA << BTN_MIDDLE << meta << true; } void PointerInputTest::testModifierClickUnrestrictedMove() { // this test ensures that Alt+mouse button press triggers unrestricted move using namespace KWayland::Client; // create pointer and signal spy for button events auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged); QVERIFY(buttonSpy.isValid()); // first modify the config for this run QFETCH(QString, modKey); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", modKey); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // move cursor on window Cursor::setPos(window->geometry().center()); // simulate modifier+click quint32 timestamp = 1; QFETCH(bool, capsLock); if (capsLock) { kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); } QFETCH(int, modifierKey); QFETCH(int, mouseButton); kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); QVERIFY(!window->isMove()); kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++); QVERIFY(window->isMove()); // release modifier should not change it kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); QVERIFY(window->isMove()); // but releasing the key should end move/resize kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++); QVERIFY(!window->isMove()); if (capsLock) { kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); } // all of that should not have triggered button events on the surface QCOMPARE(buttonSpy.count(), 0); // also waiting shouldn't give us the event QVERIFY(!buttonSpy.wait(100)); } void PointerInputTest::testModifierClickUnrestrictedMoveGlobalShortcutsDisabled() { // this test ensures that Alt+mouse button press triggers unrestricted move using namespace KWayland::Client; // create pointer and signal spy for button events auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged); QVERIFY(buttonSpy.isValid()); // first modify the config for this run KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), Qt::AltModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // disable global shortcuts QVERIFY(!workspace()->globalShortcutsDisabled()); workspace()->disableGlobalShortcutsForClient(true); QVERIFY(workspace()->globalShortcutsDisabled()); // move cursor on window Cursor::setPos(window->geometry().center()); // simulate modifier+click quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); QVERIFY(!window->isMove()); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(!window->isMove()); // release modifier should not change it kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); QVERIFY(!window->isMove()); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); workspace()->disableGlobalShortcutsForClient(false); } void PointerInputTest::testModifierScrollOpacity_data() { QTest::addColumn("modifierKey"); QTest::addColumn("modKey"); QTest::addColumn("capsLock"); const QString alt = QStringLiteral("Alt"); const QString meta = QStringLiteral("Meta"); QTest::newRow("Left Alt") << KEY_LEFTALT << alt << false; QTest::newRow("Right Alt") << KEY_RIGHTALT << alt << false; QTest::newRow("Left Meta") << KEY_LEFTMETA << meta << false; QTest::newRow("Right Meta") << KEY_RIGHTMETA << meta << false; QTest::newRow("Left Alt/CapsLock") << KEY_LEFTALT << alt << true; QTest::newRow("Right Alt/CapsLock") << KEY_RIGHTALT << alt << true; QTest::newRow("Left Meta/CapsLock") << KEY_LEFTMETA << meta << true; QTest::newRow("Right Meta/CapsLock") << KEY_RIGHTMETA << meta << true; } void PointerInputTest::testModifierScrollOpacity() { // this test verifies that mod+wheel performs a window operation and does not // pass the wheel to the window using namespace KWayland::Client; // create pointer and signal spy for button events auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy axisSpy(pointer, &Pointer::axisChanged); QVERIFY(axisSpy.isValid()); // first modify the config for this run QFETCH(QString, modKey); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", modKey); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // set the opacity to 0.5 window->setOpacity(0.5); QCOMPARE(window->opacity(), 0.5); // move cursor on window Cursor::setPos(window->geometry().center()); // simulate modifier+wheel quint32 timestamp = 1; QFETCH(bool, capsLock); if (capsLock) { kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); } QFETCH(int, modifierKey); kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(window->opacity(), 0.6); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(window->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); if (capsLock) { kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); } // axis should have been filtered out QCOMPARE(axisSpy.count(), 0); QVERIFY(!axisSpy.wait(100)); } void PointerInputTest::testModifierScrollOpacityGlobalShortcutsDisabled() { // this test verifies that mod+wheel performs a window operation and does not // pass the wheel to the window using namespace KWayland::Client; // create pointer and signal spy for button events auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy axisSpy(pointer, &Pointer::axisChanged); QVERIFY(axisSpy.isValid()); // first modify the config for this run KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // set the opacity to 0.5 window->setOpacity(0.5); QCOMPARE(window->opacity(), 0.5); // move cursor on window Cursor::setPos(window->geometry().center()); // disable global shortcuts QVERIFY(!workspace()->globalShortcutsDisabled()); workspace()->disableGlobalShortcutsForClient(true); QVERIFY(workspace()->globalShortcutsDisabled()); // simulate modifier+wheel quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(window->opacity(), 0.5); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(window->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); workspace()->disableGlobalShortcutsForClient(false); } void PointerInputTest::testScrollAction() { // this test verifies that scroll on inactive window performs a mouse action using namespace KWayland::Client; auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy axisSpy(pointer, &Pointer::axisChanged); QVERIFY(axisSpy.isValid()); // first modify the config for this run KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandWindowWheel", "activate and scroll"); group.sync(); workspace()->slotReconfigure(); // create two windows QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface1 = Test::createSurface(m_compositor); QVERIFY(surface1); ShellSurface *shellSurface1 = Test::createShellSurface(surface1, surface1); QVERIFY(shellSurface1); render(surface1); QVERIFY(clientAddedSpy.wait()); AbstractClient *window1 = workspace()->activeClient(); QVERIFY(window1); Surface *surface2 = Test::createSurface(m_compositor); QVERIFY(surface2); ShellSurface *shellSurface2 = Test::createShellSurface(surface2, surface2); QVERIFY(shellSurface2); render(surface2); QVERIFY(clientAddedSpy.wait()); AbstractClient *window2 = workspace()->activeClient(); QVERIFY(window2); QVERIFY(window1 != window2); // move cursor to the inactive window Cursor::setPos(window1->geometry().center()); quint32 timestamp = 1; QVERIFY(!window1->isActive()); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QVERIFY(window1->isActive()); // but also the wheel event should be passed to the window QVERIFY(axisSpy.wait()); // we need to wait a little bit, otherwise the test crashes in effectshandler, needs fixing QTest::qWait(100); } void PointerInputTest::testFocusFollowsMouse() { using namespace KWayland::Client; // need to create a pointer, otherwise it doesn't accept focus auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); // move cursor out of the way of first window to be created Cursor::setPos(900, 900); // first modify the config for this run KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("AutoRaise", true); group.writeEntry("AutoRaiseInterval", 20); group.writeEntry("DelayFocusInterval", 200); group.writeEntry("FocusPolicy", "FocusFollowsMouse"); group.sync(); workspace()->slotReconfigure(); // verify the settings QCOMPARE(options->focusPolicy(), Options::FocusFollowsMouse); QVERIFY(options->isAutoRaise()); QCOMPARE(options->autoRaiseInterval(), 20); QCOMPARE(options->delayFocusInterval(), 200); // create two windows QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface1 = Test::createSurface(m_compositor); QVERIFY(surface1); ShellSurface *shellSurface1 = Test::createShellSurface(surface1, surface1); QVERIFY(shellSurface1); render(surface1, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window1 = workspace()->activeClient(); QVERIFY(window1); Surface *surface2 = Test::createSurface(m_compositor); QVERIFY(surface2); ShellSurface *shellSurface2 = Test::createShellSurface(surface2, surface2); QVERIFY(shellSurface2); render(surface2, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window2 = workspace()->activeClient(); QVERIFY(window2); QVERIFY(window1 != window2); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); // geometry of the two windows should be overlapping QVERIFY(window1->geometry().intersects(window2->geometry())); // signal spies for active window changed and stacking order changed QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::clientActivated); QVERIFY(activeWindowChangedSpy.isValid()); QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); QVERIFY(stackingOrderChangedSpy.isValid()); QVERIFY(!window1->isActive()); QVERIFY(window2->isActive()); // move on top of first window QVERIFY(window1->geometry().contains(10, 10)); QVERIFY(!window2->geometry().contains(10, 10)); Cursor::setPos(10, 10); QVERIFY(stackingOrderChangedSpy.wait()); QCOMPARE(stackingOrderChangedSpy.count(), 1); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); QTRY_VERIFY(window1->isActive()); // move on second window, but move away before active window change delay hits Cursor::setPos(810, 810); QVERIFY(stackingOrderChangedSpy.wait()); QCOMPARE(stackingOrderChangedSpy.count(), 2); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); Cursor::setPos(10, 10); QVERIFY(!activeWindowChangedSpy.wait(250)); QVERIFY(window1->isActive()); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); // as we moved back on window 1 that should been raised in the mean time QCOMPARE(stackingOrderChangedSpy.count(), 3); // quickly move on window 2 and back on window 1 should not raise window 2 Cursor::setPos(810, 810); Cursor::setPos(10, 10); QVERIFY(!stackingOrderChangedSpy.wait(250)); } void PointerInputTest::testMouseActionInactiveWindow_data() { QTest::addColumn("button"); QTest::newRow("Left") << quint32(BTN_LEFT); QTest::newRow("Middle") << quint32(BTN_MIDDLE); QTest::newRow("Right") << quint32(BTN_RIGHT); } void PointerInputTest::testMouseActionInactiveWindow() { // this test performs the mouse button window action on an inactive window // it should activate the window and raise it using namespace KWayland::Client; // first modify the config for this run - disable FocusFollowsMouse KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("FocusPolicy", "ClickToFocus"); group.sync(); group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandWindow1", "Activate, raise and pass click"); group.writeEntry("CommandWindow2", "Activate, raise and pass click"); group.writeEntry("CommandWindow3", "Activate, raise and pass click"); group.sync(); workspace()->slotReconfigure(); // create two windows QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface1 = Test::createSurface(m_compositor); QVERIFY(surface1); ShellSurface *shellSurface1 = Test::createShellSurface(surface1, surface1); QVERIFY(shellSurface1); render(surface1, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window1 = workspace()->activeClient(); QVERIFY(window1); Surface *surface2 = Test::createSurface(m_compositor); QVERIFY(surface2); ShellSurface *shellSurface2 = Test::createShellSurface(surface2, surface2); QVERIFY(shellSurface2); render(surface2, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window2 = workspace()->activeClient(); QVERIFY(window2); QVERIFY(window1 != window2); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); // geometry of the two windows should be overlapping QVERIFY(window1->geometry().intersects(window2->geometry())); // signal spies for active window changed and stacking order changed QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::clientActivated); QVERIFY(activeWindowChangedSpy.isValid()); QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); QVERIFY(stackingOrderChangedSpy.isValid()); QVERIFY(!window1->isActive()); QVERIFY(window2->isActive()); // move on top of first window QVERIFY(window1->geometry().contains(10, 10)); QVERIFY(!window2->geometry().contains(10, 10)); Cursor::setPos(10, 10); // no focus follows mouse QVERIFY(!stackingOrderChangedSpy.wait(200)); QVERIFY(stackingOrderChangedSpy.isEmpty()); QVERIFY(activeWindowChangedSpy.isEmpty()); QVERIFY(window2->isActive()); // and click quint32 timestamp = 1; QFETCH(quint32, button); kwinApp()->platform()->pointerButtonPressed(button, timestamp++); // should raise window1 and activate it QCOMPARE(stackingOrderChangedSpy.count(), 1); QVERIFY(!activeWindowChangedSpy.isEmpty()); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); QVERIFY(window1->isActive()); QVERIFY(!window2->isActive()); // release again kwinApp()->platform()->pointerButtonReleased(button, timestamp++); } void PointerInputTest::testMouseActionActiveWindow_data() { QTest::addColumn("clickRaise"); QTest::addColumn("button"); for (quint32 i=BTN_LEFT; i < BTN_JOYSTICK; i++) { QByteArray number = QByteArray::number(i, 16); QTest::newRow(QByteArrayLiteral("click raise/").append(number).constData()) << true << i; QTest::newRow(QByteArrayLiteral("no click raise/").append(number).constData()) << false << i; } } void PointerInputTest::testMouseActionActiveWindow() { // this test verifies the mouse action performed on an active window // for all buttons it should trigger a window raise depending on the // click raise option using namespace KWayland::Client; // create a button spy - all clicks should be passed through auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy buttonSpy(pointer, &Pointer::buttonStateChanged); QVERIFY(buttonSpy.isValid()); // adjust config for this run QFETCH(bool, clickRaise); KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("ClickRaise", clickRaise); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->isClickRaise(), clickRaise); // create two windows QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface1 = Test::createSurface(m_compositor); QVERIFY(surface1); ShellSurface *shellSurface1 = Test::createShellSurface(surface1, surface1); QVERIFY(shellSurface1); render(surface1, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window1 = workspace()->activeClient(); QVERIFY(window1); QSignalSpy window1DestroyedSpy(window1, &QObject::destroyed); QVERIFY(window1DestroyedSpy.isValid()); Surface *surface2 = Test::createSurface(m_compositor); QVERIFY(surface2); ShellSurface *shellSurface2 = Test::createShellSurface(surface2, surface2); QVERIFY(shellSurface2); render(surface2, QSize(800, 800)); QVERIFY(clientAddedSpy.wait()); AbstractClient *window2 = workspace()->activeClient(); QVERIFY(window2); QVERIFY(window1 != window2); QSignalSpy window2DestroyedSpy(window2, &QObject::destroyed); QVERIFY(window2DestroyedSpy.isValid()); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); // geometry of the two windows should be overlapping QVERIFY(window1->geometry().intersects(window2->geometry())); // lower the currently active window workspace()->lowerClient(window2); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); // signal spy for stacking order spy QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); QVERIFY(stackingOrderChangedSpy.isValid()); // move on top of second window QVERIFY(!window1->geometry().contains(900, 900)); QVERIFY(window2->geometry().contains(900, 900)); Cursor::setPos(900, 900); // and click quint32 timestamp = 1; QFETCH(quint32, button); kwinApp()->platform()->pointerButtonPressed(button, timestamp++); QVERIFY(buttonSpy.wait()); if (clickRaise) { QCOMPARE(stackingOrderChangedSpy.count(), 1); QTRY_COMPARE_WITH_TIMEOUT(workspace()->topClientOnDesktop(1, -1), window2, 200); } else { QCOMPARE(stackingOrderChangedSpy.count(), 0); QVERIFY(!stackingOrderChangedSpy.wait(100)); QCOMPARE(workspace()->topClientOnDesktop(1, -1), window1); } // release again kwinApp()->platform()->pointerButtonReleased(button, timestamp++); delete surface1; QVERIFY(window1DestroyedSpy.wait()); delete surface2; QVERIFY(window2DestroyedSpy.wait()); } void PointerInputTest::testCursorImage() { // this test verifies that the pointer image gets updated correctly from the client provided data using namespace KWayland::Client; // we need a pointer to get the enter event auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); // move cursor somewhere the new window won't open Cursor::setPos(800, 800); auto p = input()->pointer(); // at the moment it should be the fallback cursor const QImage fallbackCursor = p->cursorImage(); QVERIFY(!fallbackCursor.isNull()); // create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // move cursor to center of window, this should first set a null pointer, so we still show old cursor Cursor::setPos(window->geometry().center()); - QCOMPARE(p->window().data(), window); + QCOMPARE(p->focus().data(), window); QCOMPARE(p->cursorImage(), fallbackCursor); QVERIFY(enteredSpy.wait()); // create a cursor on the pointer Surface *cursorSurface = Test::createSurface(m_compositor); QVERIFY(cursorSurface); QSignalSpy cursorRenderedSpy(cursorSurface, &Surface::frameRendered); QVERIFY(cursorRenderedSpy.isValid()); QImage red = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); red.fill(Qt::red); cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(red)); cursorSurface->damage(QRect(0, 0, 10, 10)); cursorSurface->commit(); pointer->setCursor(cursorSurface, QPoint(5, 5)); QVERIFY(cursorRenderedSpy.wait()); QCOMPARE(p->cursorImage(), red); QCOMPARE(p->cursorHotSpot(), QPoint(5, 5)); // change hotspot pointer->setCursor(cursorSurface, QPoint(6, 6)); Test::flushWaylandConnection(); QTRY_COMPARE(p->cursorHotSpot(), QPoint(6, 6)); QCOMPARE(p->cursorImage(), red); // change the buffer QImage blue = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); blue.fill(Qt::blue); auto b = Test::waylandShmPool()->createBuffer(blue); cursorSurface->attachBuffer(b); cursorSurface->damage(QRect(0, 0, 10, 10)); cursorSurface->commit(); QVERIFY(cursorRenderedSpy.wait()); QTRY_COMPARE(p->cursorImage(), blue); QCOMPARE(p->cursorHotSpot(), QPoint(6, 6)); // scaled cursor QImage blueScaled = QImage(QSize(20, 20), QImage::Format_ARGB32_Premultiplied); blueScaled.fill(Qt::blue); auto bs = Test::waylandShmPool()->createBuffer(blueScaled); cursorSurface->attachBuffer(bs); cursorSurface->setScale(2); cursorSurface->damage(QRect(0, 0, 20, 20)); cursorSurface->commit(); QVERIFY(cursorRenderedSpy.wait()); QTRY_COMPARE(p->cursorImage(), blueScaled); QCOMPARE(p->cursorImage().devicePixelRatio(), 2.0); QCOMPARE(p->cursorHotSpot(), QPoint(6, 6)); //surface-local (so not changed) // hide the cursor pointer->setCursor(nullptr); Test::flushWaylandConnection(); QTRY_VERIFY(p->cursorImage().isNull()); // move cursor somewhere else, should reset to fallback cursor Cursor::setPos(window->geometry().bottomLeft() + QPoint(20, 20)); - QVERIFY(p->window().isNull()); + QVERIFY(p->focus().isNull()); QVERIFY(!p->cursorImage().isNull()); QCOMPARE(p->cursorImage(), fallbackCursor); } class HelperEffect : public Effect { Q_OBJECT public: HelperEffect() {} ~HelperEffect() {} }; void PointerInputTest::testEffectOverrideCursorImage() { // this test verifies the effect cursor override handling using namespace KWayland::Client; // we need a pointer to get the enter event and set a cursor auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(pointer, &Pointer::left); QVERIFY(leftSpy.isValid()); // move cursor somewhere the new window won't open Cursor::setPos(800, 800); auto p = input()->pointer(); // here we should have the fallback cursor const QImage fallback = p->cursorImage(); QVERIFY(!fallback.isNull()); // now let's create a window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // and move cursor to the window QVERIFY(!window->geometry().contains(QPoint(800, 800))); Cursor::setPos(window->geometry().center()); QVERIFY(enteredSpy.wait()); // cursor image should still be fallback QCOMPARE(p->cursorImage(), fallback); // now create an effect and set an override cursor QScopedPointer effect(new HelperEffect); effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); const QImage sizeAll = p->cursorImage(); QVERIFY(!sizeAll.isNull()); QVERIFY(sizeAll != fallback); QVERIFY(leftSpy.wait()); // let's change to arrow cursor, this should be our fallback effects->defineCursor(Qt::ArrowCursor); QCOMPARE(p->cursorImage(), fallback); // back to size all effects->defineCursor(Qt::SizeAllCursor); QCOMPARE(p->cursorImage(), sizeAll); // move cursor outside the window area Cursor::setPos(800, 800); // and end the override, which should switch to fallback effects->stopMouseInterception(effect.data()); QCOMPARE(p->cursorImage(), fallback); // start mouse interception again effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); QCOMPARE(p->cursorImage(), sizeAll); // move cursor to area of window Cursor::setPos(window->geometry().center()); // this should not result in an enter event QVERIFY(!enteredSpy.wait(100)); // after ending the interception we should get an enter event effects->stopMouseInterception(effect.data()); QVERIFY(enteredSpy.wait()); QVERIFY(p->cursorImage().isNull()); } void PointerInputTest::testPopup() { // this test validates the basic popup behavior // a button press outside the window should dismiss the popup // first create a parent surface using namespace KWayland::Client; auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(pointer, &Pointer::left); QVERIFY(leftSpy.isValid()); QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged); QVERIFY(buttonStateChangedSpy.isValid()); QSignalSpy motionSpy(pointer, &Pointer::motion); QVERIFY(motionSpy.isValid()); Cursor::setPos(800, 800); QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); QCOMPARE(window->hasPopupGrab(), false); // move pointer into window QVERIFY(!window->geometry().contains(QPoint(800, 800))); Cursor::setPos(window->geometry().center()); QVERIFY(enteredSpy.wait()); // click inside window to create serial quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(buttonStateChangedSpy.wait()); // now create the popup surface Surface *popupSurface = Test::createSurface(m_compositor); QVERIFY(popupSurface); ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); QVERIFY(popupShellSurface); QSignalSpy popupDoneSpy(popupShellSurface, &ShellSurface::popupDone); QVERIFY(popupDoneSpy.isValid()); // TODO: proper serial popupShellSurface->setTransientPopup(surface, m_seat, 0, QPoint(80, 20)); render(popupSurface); QVERIFY(clientAddedSpy.wait()); auto popupClient = clientAddedSpy.last().first().value(); QVERIFY(popupClient); QVERIFY(popupClient != window); QCOMPARE(window, workspace()->activeClient()); QCOMPARE(popupClient->transientFor(), window); QCOMPARE(popupClient->pos(), window->pos() + QPoint(80, 20)); QCOMPARE(popupClient->hasPopupGrab(), true); // let's move the pointer into the center of the window Cursor::setPos(popupClient->geometry().center()); QVERIFY(enteredSpy.wait()); QCOMPARE(enteredSpy.count(), 2); QCOMPARE(leftSpy.count(), 1); QCOMPARE(pointer->enteredSurface(), popupSurface); // let's move the pointer outside of the popup window // this should not really change anything, it gets a leave event Cursor::setPos(popupClient->geometry().bottomRight() + QPoint(2, 2)); QVERIFY(leftSpy.wait()); QCOMPARE(leftSpy.count(), 2); QVERIFY(popupDoneSpy.isEmpty()); // now click, should trigger popupDone kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(popupDoneSpy.wait()); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); } void PointerInputTest::testDecoCancelsPopup() { // this test verifies that clicking the window decoration of parent window // cancels the popup // first create a parent surface using namespace KWayland::Client; auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(pointer, &Pointer::left); QVERIFY(leftSpy.isValid()); QSignalSpy buttonStateChangedSpy(pointer, &Pointer::buttonStateChanged); QVERIFY(buttonStateChangedSpy.isValid()); QSignalSpy motionSpy(pointer, &Pointer::motion); QVERIFY(motionSpy.isValid()); Cursor::setPos(800, 800); QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); auto deco = Test::waylandServerSideDecoration()->create(surface, surface); QSignalSpy decoSpy(deco, &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); deco->requestMode(ServerSideDecoration::Mode::Server); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); QCOMPARE(window->hasPopupGrab(), false); QVERIFY(window->isDecorated()); // move pointer into window QVERIFY(!window->geometry().contains(QPoint(800, 800))); Cursor::setPos(window->geometry().center()); QVERIFY(enteredSpy.wait()); // click inside window to create serial quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(buttonStateChangedSpy.wait()); // now create the popup surface Surface *popupSurface = Test::createSurface(m_compositor); QVERIFY(popupSurface); ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); QVERIFY(popupShellSurface); QSignalSpy popupDoneSpy(popupShellSurface, &ShellSurface::popupDone); QVERIFY(popupDoneSpy.isValid()); // TODO: proper serial popupShellSurface->setTransientPopup(surface, m_seat, 0, QPoint(80, 20)); render(popupSurface); QVERIFY(clientAddedSpy.wait()); auto popupClient = clientAddedSpy.last().first().value(); QVERIFY(popupClient); QVERIFY(popupClient != window); QCOMPARE(window, workspace()->activeClient()); QCOMPARE(popupClient->transientFor(), window); QCOMPARE(popupClient->pos(), window->pos() + window->clientPos() + QPoint(80, 20)); QCOMPARE(popupClient->hasPopupGrab(), true); // let's move the pointer into the center of the deco Cursor::setPos(window->geometry().center().x(), window->y() + (window->height() - window->clientSize().height()) / 2); kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); QVERIFY(popupDoneSpy.wait()); kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); } void PointerInputTest::testWindowUnderCursorWhileButtonPressed() { // this test verifies that opening a window underneath the mouse cursor does not // trigger a leave event if a button is pressed // see BUG: 372876 // first create a parent surface using namespace KWayland::Client; auto pointer = m_seat->createPointer(m_seat); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy enteredSpy(pointer, &Pointer::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(pointer, &Pointer::left); QVERIFY(leftSpy.isValid()); Cursor::setPos(800, 800); QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); Surface *surface = Test::createSurface(m_compositor); QVERIFY(surface); ShellSurface *shellSurface = Test::createShellSurface(surface, surface); QVERIFY(shellSurface); render(surface); QVERIFY(clientAddedSpy.wait()); AbstractClient *window = workspace()->activeClient(); QVERIFY(window); // move cursor over window QVERIFY(!window->geometry().contains(QPoint(800, 800))); Cursor::setPos(window->geometry().center()); QVERIFY(enteredSpy.wait()); // click inside window quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); // now create a second window as transient Surface *popupSurface = Test::createSurface(m_compositor); QVERIFY(popupSurface); ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); QVERIFY(popupShellSurface); popupShellSurface->setTransient(surface, QPoint(0, 0)); render(popupSurface); QVERIFY(clientAddedSpy.wait()); auto popupClient = clientAddedSpy.last().first().value(); QVERIFY(popupClient); QVERIFY(popupClient != window); QCOMPARE(window->geometry(), popupClient->geometry()); QVERIFY(!leftSpy.wait()); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); // now that the button is no longer pressed we should get the leave event QVERIFY(leftSpy.wait()); QCOMPARE(leftSpy.count(), 1); QCOMPARE(enteredSpy.count(), 2); } void PointerInputTest::testConfineToScreenGeometry_data() { QTest::addColumn("startPos"); QTest::addColumn("targetPos"); QTest::addColumn("expectedPos"); // screen layout: // // +----------+----------+---------+ // | left | top | right | // +----------+----------+---------+ // | bottom | // +----------+ // QTest::newRow("move top-left - left screen") << QPoint(640, 512) << QPoint(-100, -100) << QPoint(0, 0); QTest::newRow("move top - left screen") << QPoint(640, 512) << QPoint(640, -100) << QPoint(640, 0); QTest::newRow("move top-right - left screen") << QPoint(640, 512) << QPoint(1380, -100) << QPoint(1380, 0); QTest::newRow("move right - left screen") << QPoint(640, 512) << QPoint(1380, 512) << QPoint(1380, 512); QTest::newRow("move bottom-right - left screen") << QPoint(640, 512) << QPoint(1380, 1124) << QPoint(1380, 1124); QTest::newRow("move bottom - left screen") << QPoint(640, 512) << QPoint(640, 1124) << QPoint(640, 1023); QTest::newRow("move bottom-left - left screen") << QPoint(640, 512) << QPoint(-100, 1124) << QPoint(0, 1023); QTest::newRow("move left - left screen") << QPoint(640, 512) << QPoint(-100, 512) << QPoint(0, 512); QTest::newRow("move top-left - top screen") << QPoint(1920, 512) << QPoint(1180, -100) << QPoint(1180, 0); QTest::newRow("move top - top screen") << QPoint(1920, 512) << QPoint(1920, -100) << QPoint(1920, 0); QTest::newRow("move top-right - top screen") << QPoint(1920, 512) << QPoint(2660, -100) << QPoint(2660, 0); QTest::newRow("move right - top screen") << QPoint(1920, 512) << QPoint(2660, 512) << QPoint(2660, 512); QTest::newRow("move bottom-right - top screen") << QPoint(1920, 512) << QPoint(2660, 1124) << QPoint(2559, 1023); QTest::newRow("move bottom - top screen") << QPoint(1920, 512) << QPoint(1920, 1124) << QPoint(1920, 1124); QTest::newRow("move bottom-left - top screen") << QPoint(1920, 512) << QPoint(1180, 1124) << QPoint(1280, 1023); QTest::newRow("move left - top screen") << QPoint(1920, 512) << QPoint(1180, 512) << QPoint(1180, 512); QTest::newRow("move top-left - right screen") << QPoint(3200, 512) << QPoint(2460, -100) << QPoint(2460, 0); QTest::newRow("move top - right screen") << QPoint(3200, 512) << QPoint(3200, -100) << QPoint(3200, 0); QTest::newRow("move top-right - right screen") << QPoint(3200, 512) << QPoint(3940, -100) << QPoint(3839, 0); QTest::newRow("move right - right screen") << QPoint(3200, 512) << QPoint(3940, 512) << QPoint(3839, 512); QTest::newRow("move bottom-right - right screen") << QPoint(3200, 512) << QPoint(3940, 1124) << QPoint(3839, 1023); QTest::newRow("move bottom - right screen") << QPoint(3200, 512) << QPoint(3200, 1124) << QPoint(3200, 1023); QTest::newRow("move bottom-left - right screen") << QPoint(3200, 512) << QPoint(2460, 1124) << QPoint(2460, 1124); QTest::newRow("move left - right screen") << QPoint(3200, 512) << QPoint(2460, 512) << QPoint(2460, 512); QTest::newRow("move top-left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 924) << QPoint(1180, 924); QTest::newRow("move top - bottom screen") << QPoint(1920, 1536) << QPoint(1920, 924) << QPoint(1920, 924); QTest::newRow("move top-right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 924) << QPoint(2660, 924); QTest::newRow("move right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 1536) << QPoint(2559, 1536); QTest::newRow("move bottom-right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 2148) << QPoint(2559, 2047); QTest::newRow("move bottom - bottom screen") << QPoint(1920, 1536) << QPoint(1920, 2148) << QPoint(1920, 2047); QTest::newRow("move bottom-left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 2148) << QPoint(1280, 2047); QTest::newRow("move left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 1536) << QPoint(1280, 1536); } void PointerInputTest::testConfineToScreenGeometry() { // this test verifies that pointer belongs to at least one screen // after moving it to off-screen area // unload the Present Windows effect because it pushes back // pointer if it's at (0, 0) static_cast(effects)->unloadEffect(QStringLiteral("presentwindows")); // setup screen layout const QVector geometries { QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024), QRect(2560, 0, 1280, 1024), QRect(1280, 1024, 1280, 1024) }; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, geometries.count()), Q_ARG(QVector, geometries)); QCOMPARE(screens()->count(), geometries.count()); QCOMPARE(screens()->geometry(0), geometries.at(0)); QCOMPARE(screens()->geometry(1), geometries.at(1)); QCOMPARE(screens()->geometry(2), geometries.at(2)); QCOMPARE(screens()->geometry(3), geometries.at(3)); // move pointer to initial position QFETCH(QPoint, startPos); Cursor::setPos(startPos); QCOMPARE(Cursor::pos(), startPos); // perform movement QFETCH(QPoint, targetPos); kwinApp()->platform()->pointerMotion(targetPos, 1); QFETCH(QPoint, expectedPos); QCOMPARE(Cursor::pos(), expectedPos); } } WAYLANDTEST_MAIN(KWin::PointerInputTest) #include "pointer_input.moc" diff --git a/autotests/integration/window_selection_test.cpp b/autotests/integration/window_selection_test.cpp index 3b70a37a7..d163c809d 100644 --- a/autotests/integration/window_selection_test.cpp +++ b/autotests/integration/window_selection_test.cpp @@ -1,559 +1,559 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "cursor.h" #include "keyboard_input.h" #include "platform.h" #include "pointer_input.h" #include "shell_client.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_window_selection-0"); class TestWindowSelection : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testSelectOnWindowPointer(); void testSelectOnWindowKeyboard_data(); void testSelectOnWindowKeyboard(); void testSelectOnWindowTouch(); void testCancelOnWindowPointer(); void testCancelOnWindowKeyboard(); void testSelectPointPointer(); void testSelectPointTouch(); }; void TestWindowSelection::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); qputenv("XKB_DEFAULT_RULES", "evdev"); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); waylandServer()->initWorkspace(); } void TestWindowSelection::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandPointer()); screens()->setCurrent(0); KWin::Cursor::setPos(QPoint(1280, 512)); } void TestWindowSelection::cleanup() { Test::destroyWaylandConnection(); } void TestWindowSelection::testSelectOnWindowPointer() { // this test verifies window selection through pointer works QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(keyboardEnteredSpy.isValid()); QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); QVERIFY(keyboardLeftSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(keyboardEnteredSpy.wait()); KWin::Cursor::setPos(client->geometry().center()); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); QVERIFY(pointerEnteredSpy.wait()); Toplevel *selectedWindow = nullptr; auto callback = [&selectedWindow] (Toplevel *t) { selectedWindow = t; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); QCOMPARE(keyboardLeftSpy.count(), 0); QVERIFY(pointerLeftSpy.wait()); if (keyboardLeftSpy.isEmpty()) { QVERIFY(keyboardLeftSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); // simulate left button press quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); // should not have ended the mode QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); - QVERIFY(input()->pointer()->window().isNull()); + QVERIFY(input()->pointer()->focus().isNull()); // updating the pointer should not change anything input()->pointer()->update(); - QVERIFY(input()->pointer()->window().isNull()); + QVERIFY(input()->pointer()->focus().isNull()); // updating keyboard should also not change input()->keyboard()->update(); // perform a right button click kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); // should not have ended the mode QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); // now release kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(selectedWindow, client); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); // should give back keyboard and pointer QVERIFY(pointerEnteredSpy.wait()); if (keyboardEnteredSpy.count() != 2) { QVERIFY(keyboardEnteredSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); QCOMPARE(pointerEnteredSpy.count(), 2); QCOMPARE(keyboardEnteredSpy.count(), 2); } void TestWindowSelection::testSelectOnWindowKeyboard_data() { QTest::addColumn("key"); QTest::newRow("enter") << KEY_ENTER; QTest::newRow("keypad enter") << KEY_KPENTER; QTest::newRow("space") << KEY_SPACE; } void TestWindowSelection::testSelectOnWindowKeyboard() { // this test verifies window selection through keyboard key QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(keyboardEnteredSpy.isValid()); QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); QVERIFY(keyboardLeftSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(keyboardEnteredSpy.wait()); QVERIFY(!client->geometry().contains(KWin::Cursor::pos())); Toplevel *selectedWindow = nullptr; auto callback = [&selectedWindow] (Toplevel *t) { selectedWindow = t; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); QCOMPARE(keyboardLeftSpy.count(), 0); QVERIFY(keyboardLeftSpy.wait()); QCOMPARE(pointerLeftSpy.count(), 0); QCOMPARE(keyboardLeftSpy.count(), 1); // simulate key press quint32 timestamp = 0; // move cursor through keys auto keyPress = [×tamp] (qint32 key) { kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); }; while (KWin::Cursor::pos().x() >= client->geometry().x() + client->geometry().width()) { keyPress(KEY_LEFT); } while (KWin::Cursor::pos().x() <= client->geometry().x()) { keyPress(KEY_RIGHT); } while (KWin::Cursor::pos().y() <= client->geometry().y()) { keyPress(KEY_DOWN); } while (KWin::Cursor::pos().y() >= client->geometry().y() + client->geometry().height()) { keyPress(KEY_UP); } QFETCH(qint32, key); kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(selectedWindow, client); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); // should give back keyboard and pointer QVERIFY(pointerEnteredSpy.wait()); if (keyboardEnteredSpy.count() != 2) { QVERIFY(keyboardEnteredSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 0); QCOMPARE(keyboardLeftSpy.count(), 1); QCOMPARE(pointerEnteredSpy.count(), 1); QCOMPARE(keyboardEnteredSpy.count(), 2); kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); } void TestWindowSelection::testSelectOnWindowTouch() { // this test verifies window selection through touch QScopedPointer touch(Test::waylandSeat()->createTouch()); QSignalSpy touchStartedSpy(touch.data(), &Touch::sequenceStarted); QVERIFY(touchStartedSpy.isValid()); QSignalSpy touchCanceledSpy(touch.data(), &Touch::sequenceCanceled); QVERIFY(touchCanceledSpy.isValid()); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); Toplevel *selectedWindow = nullptr; auto callback = [&selectedWindow] (Toplevel *t) { selectedWindow = t; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); // simulate touch down quint32 timestamp = 0; kwinApp()->platform()->touchDown(0, client->geometry().center(), timestamp++); QVERIFY(!selectedWindow); kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(selectedWindow, client); // with movement selectedWindow = nullptr; kwinApp()->platform()->startInteractiveWindowSelection(callback); kwinApp()->platform()->touchDown(0, client->geometry().bottomRight() + QPoint(20, 20), timestamp++); QVERIFY(!selectedWindow); kwinApp()->platform()->touchMotion(0, client->geometry().bottomRight() - QPoint(1, 1), timestamp++); QVERIFY(!selectedWindow); kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(selectedWindow, client); QCOMPARE(input()->isSelectingWindow(), false); // it cancels active touch sequence on the window kwinApp()->platform()->touchDown(0, client->geometry().center(), timestamp++); QVERIFY(touchStartedSpy.wait()); selectedWindow = nullptr; kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(touchCanceledSpy.wait()); QVERIFY(!selectedWindow); // this touch up does not yet select the window, it was started prior to the selection kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(!selectedWindow); kwinApp()->platform()->touchDown(0, client->geometry().center(), timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(selectedWindow, client); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(touchStartedSpy.count(), 1); QCOMPARE(touchCanceledSpy.count(), 1); } void TestWindowSelection::testCancelOnWindowPointer() { // this test verifies that window selection cancels through right button click QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(keyboardEnteredSpy.isValid()); QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); QVERIFY(keyboardLeftSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(keyboardEnteredSpy.wait()); KWin::Cursor::setPos(client->geometry().center()); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); QVERIFY(pointerEnteredSpy.wait()); Toplevel *selectedWindow = nullptr; auto callback = [&selectedWindow] (Toplevel *t) { selectedWindow = t; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); QCOMPARE(keyboardLeftSpy.count(), 0); QVERIFY(pointerLeftSpy.wait()); if (keyboardLeftSpy.isEmpty()) { QVERIFY(keyboardLeftSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); // simulate left button press quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QVERIFY(!selectedWindow); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); // should give back keyboard and pointer QVERIFY(pointerEnteredSpy.wait()); if (keyboardEnteredSpy.count() != 2) { QVERIFY(keyboardEnteredSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); QCOMPARE(pointerEnteredSpy.count(), 2); QCOMPARE(keyboardEnteredSpy.count(), 2); } void TestWindowSelection::testCancelOnWindowKeyboard() { // this test verifies that cancel window selection through escape key works QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(keyboardEnteredSpy.isValid()); QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); QVERIFY(keyboardLeftSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(keyboardEnteredSpy.wait()); KWin::Cursor::setPos(client->geometry().center()); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); QVERIFY(pointerEnteredSpy.wait()); Toplevel *selectedWindow = nullptr; auto callback = [&selectedWindow] (Toplevel *t) { selectedWindow = t; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractiveWindowSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QVERIFY(!selectedWindow); QCOMPARE(keyboardLeftSpy.count(), 0); QVERIFY(pointerLeftSpy.wait()); if (keyboardLeftSpy.isEmpty()) { QVERIFY(keyboardLeftSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); // simulate left button press quint32 timestamp = 0; kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QVERIFY(!selectedWindow); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); // should give back keyboard and pointer QVERIFY(pointerEnteredSpy.wait()); if (keyboardEnteredSpy.count() != 2) { QVERIFY(keyboardEnteredSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); QCOMPARE(pointerEnteredSpy.count(), 2); QCOMPARE(keyboardEnteredSpy.count(), 2); kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); } void TestWindowSelection::testSelectPointPointer() { // this test verifies point selection through pointer works QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(surface.data())); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(keyboardEnteredSpy.isValid()); QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); QVERIFY(keyboardLeftSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(keyboardEnteredSpy.wait()); KWin::Cursor::setPos(client->geometry().center()); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); QVERIFY(pointerEnteredSpy.wait()); QPoint point; auto callback = [&point] (const QPoint &p) { point = p; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractivePositionSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QCOMPARE(point, QPoint()); QCOMPARE(keyboardLeftSpy.count(), 0); QVERIFY(pointerLeftSpy.wait()); if (keyboardLeftSpy.isEmpty()) { QVERIFY(keyboardLeftSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); // trying again should not be allowed QPoint point2; kwinApp()->platform()->startInteractivePositionSelection([&point2] (const QPoint &p) { point2 = p; }); QCOMPARE(point2, QPoint(-1, -1)); // simulate left button press quint32 timestamp = 0; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); // should not have ended the mode QCOMPARE(input()->isSelectingWindow(), true); QCOMPARE(point, QPoint()); - QVERIFY(input()->pointer()->window().isNull()); + QVERIFY(input()->pointer()->focus().isNull()); // updating the pointer should not change anything input()->pointer()->update(); - QVERIFY(input()->pointer()->window().isNull()); + QVERIFY(input()->pointer()->focus().isNull()); // updating keyboard should also not change input()->keyboard()->update(); // perform a right button click kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); // should not have ended the mode QCOMPARE(input()->isSelectingWindow(), true); QCOMPARE(point, QPoint()); // now release kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(point, input()->globalPointer().toPoint()); - QCOMPARE(input()->pointer()->window().data(), client); + QCOMPARE(input()->pointer()->focus().data(), client); // should give back keyboard and pointer QVERIFY(pointerEnteredSpy.wait()); if (keyboardEnteredSpy.count() != 2) { QVERIFY(keyboardEnteredSpy.wait()); } QCOMPARE(pointerLeftSpy.count(), 1); QCOMPARE(keyboardLeftSpy.count(), 1); QCOMPARE(pointerEnteredSpy.count(), 2); QCOMPARE(keyboardEnteredSpy.count(), 2); } void TestWindowSelection::testSelectPointTouch() { // this test verifies point selection through touch works QPoint point; auto callback = [&point] (const QPoint &p) { point = p; }; // start the interaction QCOMPARE(input()->isSelectingWindow(), false); kwinApp()->platform()->startInteractivePositionSelection(callback); QCOMPARE(input()->isSelectingWindow(), true); QCOMPARE(point, QPoint()); // let's create multiple touch points quint32 timestamp = 0; kwinApp()->platform()->touchDown(0, QPointF(0, 1), timestamp++); QCOMPARE(input()->isSelectingWindow(), true); kwinApp()->platform()->touchDown(1, QPointF(10, 20), timestamp++); QCOMPARE(input()->isSelectingWindow(), true); kwinApp()->platform()->touchDown(2, QPointF(30, 40), timestamp++); QCOMPARE(input()->isSelectingWindow(), true); // let's move our points kwinApp()->platform()->touchMotion(0, QPointF(5, 10), timestamp++); kwinApp()->platform()->touchMotion(2, QPointF(20, 25), timestamp++); kwinApp()->platform()->touchMotion(1, QPointF(25, 35), timestamp++); QCOMPARE(input()->isSelectingWindow(), true); kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(input()->isSelectingWindow(), true); kwinApp()->platform()->touchUp(2, timestamp++); QCOMPARE(input()->isSelectingWindow(), true); kwinApp()->platform()->touchUp(1, timestamp++); QCOMPARE(input()->isSelectingWindow(), false); QCOMPARE(point, QPoint(25, 35)); } WAYLANDTEST_MAIN(TestWindowSelection) #include "window_selection_test.moc" diff --git a/input.cpp b/input.cpp index 98f7406b2..d1baacfe2 100644 --- a/input.cpp +++ b/input.cpp @@ -1,2191 +1,2271 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "input.h" #include "input_event.h" #include "input_event_spy.h" #include "keyboard_input.h" #include "pointer_input.h" #include "touch_input.h" #include "client.h" #include "effects.h" #include "gestures.h" #include "globalshortcuts.h" #include "logind.h" #include "main.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox/tabbox.h" #endif #include "unmanaged.h" #include "screenedge.h" #include "screens.h" #include "workspace.h" #include "libinput/connection.h" #include "libinput/device.h" #include "platform.h" #include "popup_input_filter.h" #include "shell_client.h" #include "wayland_server.h" #include #include #include #include #include #include #include //screenlocker #include // Qt #include #include namespace KWin { InputEventFilter::InputEventFilter() = default; InputEventFilter::~InputEventFilter() { if (input()) { input()->uninstallInputEventFilter(this); } } bool InputEventFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) { Q_UNUSED(event) Q_UNUSED(nativeButton) return false; } bool InputEventFilter::wheelEvent(QWheelEvent *event) { Q_UNUSED(event) return false; } bool InputEventFilter::keyEvent(QKeyEvent *event) { Q_UNUSED(event) return false; } bool InputEventFilter::touchDown(quint32 id, const QPointF &point, quint32 time) { Q_UNUSED(id) Q_UNUSED(point) Q_UNUSED(time) return false; } bool InputEventFilter::touchMotion(quint32 id, const QPointF &point, quint32 time) { Q_UNUSED(id) Q_UNUSED(point) Q_UNUSED(time) return false; } bool InputEventFilter::touchUp(quint32 id, quint32 time) { Q_UNUSED(id) Q_UNUSED(time) return false; } bool InputEventFilter::pinchGestureBegin(int fingerCount, quint32 time) { Q_UNUSED(fingerCount) Q_UNUSED(time) return false; } bool InputEventFilter::pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) { Q_UNUSED(scale) Q_UNUSED(angleDelta) Q_UNUSED(delta) Q_UNUSED(time) return false; } bool InputEventFilter::pinchGestureEnd(quint32 time) { Q_UNUSED(time) return false; } bool InputEventFilter::pinchGestureCancelled(quint32 time) { Q_UNUSED(time) return false; } bool InputEventFilter::swipeGestureBegin(int fingerCount, quint32 time) { Q_UNUSED(fingerCount) Q_UNUSED(time) return false; } bool InputEventFilter::swipeGestureUpdate(const QSizeF &delta, quint32 time) { Q_UNUSED(delta) Q_UNUSED(time) return false; } bool InputEventFilter::swipeGestureEnd(quint32 time) { Q_UNUSED(time) return false; } bool InputEventFilter::swipeGestureCancelled(quint32 time) { Q_UNUSED(time) return false; } bool InputEventFilter::switchEvent(SwitchEvent *event) { Q_UNUSED(event) return false; } void InputEventFilter::passToWaylandServer(QKeyEvent *event) { Q_ASSERT(waylandServer()); if (event->isAutoRepeat()) { return; } switch (event->type()) { case QEvent::KeyPress: waylandServer()->seat()->keyPressed(event->nativeScanCode()); break; case QEvent::KeyRelease: waylandServer()->seat()->keyReleased(event->nativeScanCode()); break; default: break; } } class VirtualTerminalFilter : public InputEventFilter { public: bool keyEvent(QKeyEvent *event) override { // really on press and not on release? X11 switches on press. if (event->type() == QEvent::KeyPress && !event->isAutoRepeat()) { const xkb_keysym_t keysym = event->nativeVirtualKey(); if (keysym >= XKB_KEY_XF86Switch_VT_1 && keysym <= XKB_KEY_XF86Switch_VT_12) { LogindIntegration::self()->switchVirtualTerminal(keysym - XKB_KEY_XF86Switch_VT_1 + 1); return true; } } return false; } }; class TerminateServerFilter : public InputEventFilter { public: bool keyEvent(QKeyEvent *event) override { if (event->type() == QEvent::KeyPress && !event->isAutoRepeat()) { if (event->nativeVirtualKey() == XKB_KEY_Terminate_Server) { qCWarning(KWIN_CORE) << "Request to terminate server"; QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection); return true; } } return false; } }; class LockScreenFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { if (!waylandServer()->isScreenLocked()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(event->timestamp()); if (event->type() == QEvent::MouseMove) { - input()->pointer()->update(); if (pointerSurfaceAllowed()) { + // TODO: should the pointer position always stay in sync, i.e. not do the check? seat->setPointerPos(event->screenPos().toPoint()); } } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { if (pointerSurfaceAllowed()) { + // TODO: can we leak presses/releases here when we move the mouse in between from an allowed surface to + // disallowed one or vice versa? event->type() == QEvent::MouseButtonPress ? seat->pointerButtonPressed(nativeButton) : seat->pointerButtonReleased(nativeButton); } } return true; } bool wheelEvent(QWheelEvent *event) override { if (!waylandServer()->isScreenLocked()) { return false; } auto seat = waylandServer()->seat(); if (pointerSurfaceAllowed()) { seat->setTimestamp(event->timestamp()); const Qt::Orientation orientation = event->angleDelta().x() == 0 ? Qt::Vertical : Qt::Horizontal; seat->pointerAxis(orientation, orientation == Qt::Horizontal ? event->angleDelta().x() : event->angleDelta().y()); } return true; } bool keyEvent(QKeyEvent * event) override { if (!waylandServer()->isScreenLocked()) { return false; } if (event->isAutoRepeat()) { // wayland client takes care of it return true; } // send event to KSldApp for global accel // if event is set to accepted it means a whitelisted shortcut was triggered // in that case we filter it out and don't process it further event->setAccepted(false); QCoreApplication::sendEvent(ScreenLocker::KSldApp::self(), event); if (event->isAccepted()) { return true; } // continue normal processing input()->keyboard()->update(); auto seat = waylandServer()->seat(); seat->setTimestamp(event->timestamp()); if (!keyboardSurfaceAllowed()) { // don't pass event to seat return true; } switch (event->type()) { case QEvent::KeyPress: seat->keyPressed(event->nativeScanCode()); break; case QEvent::KeyRelease: seat->keyReleased(event->nativeScanCode()); break; default: break; } return true; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { if (!waylandServer()->isScreenLocked()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); - if (!seat->isTouchSequence()) { - input()->touch()->update(pos); - } if (touchSurfaceAllowed()) { input()->touch()->insertId(id, seat->touchDown(pos)); } return true; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { if (!waylandServer()->isScreenLocked()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); if (touchSurfaceAllowed()) { const qint32 kwaylandId = input()->touch()->mappedId(id); if (kwaylandId != -1) { seat->touchMove(kwaylandId, pos); } } return true; } bool touchUp(quint32 id, quint32 time) override { if (!waylandServer()->isScreenLocked()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); if (touchSurfaceAllowed()) { const qint32 kwaylandId = input()->touch()->mappedId(id); if (kwaylandId != -1) { seat->touchUp(kwaylandId); input()->touch()->removeId(id); } } return true; } bool pinchGestureBegin(int fingerCount, quint32 time) override { Q_UNUSED(fingerCount) Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) override { Q_UNUSED(scale) Q_UNUSED(angleDelta) Q_UNUSED(delta) Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool pinchGestureEnd(quint32 time) override { Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool pinchGestureCancelled(quint32 time) override { Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool swipeGestureBegin(int fingerCount, quint32 time) override { Q_UNUSED(fingerCount) Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { Q_UNUSED(delta) Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool swipeGestureEnd(quint32 time) override { Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } bool swipeGestureCancelled(quint32 time) override { Q_UNUSED(time) // no touchpad multi-finger gestures on lock screen return waylandServer()->isScreenLocked(); } private: bool surfaceAllowed(KWayland::Server::SurfaceInterface *(KWayland::Server::SeatInterface::*method)() const) const { if (KWayland::Server::SurfaceInterface *s = (waylandServer()->seat()->*method)()) { if (Toplevel *t = waylandServer()->findClient(s)) { return t->isLockScreen() || t->isInputMethod(); } return false; } return true; } bool pointerSurfaceAllowed() const { return surfaceAllowed(&KWayland::Server::SeatInterface::focusedPointerSurface); } bool keyboardSurfaceAllowed() const { return surfaceAllowed(&KWayland::Server::SeatInterface::focusedKeyboardSurface); } bool touchSurfaceAllowed() const { return surfaceAllowed(&KWayland::Server::SeatInterface::focusedTouchSurface); } }; class EffectsFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) if (!effects) { return false; } return static_cast(effects)->checkInputWindowEvent(event); } bool keyEvent(QKeyEvent *event) override { if (!effects || !static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab()) { return false; } waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); passToWaylandServer(event); static_cast< EffectsHandlerImpl* >(effects)->grabbedKeyboardEvent(event); return true; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { if (!effects) { return false; } return static_cast< EffectsHandlerImpl* >(effects)->touchDown(id, pos, time); } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { if (!effects) { return false; } return static_cast< EffectsHandlerImpl* >(effects)->touchMotion(id, pos, time); } bool touchUp(quint32 id, quint32 time) override { if (!effects) { return false; } return static_cast< EffectsHandlerImpl* >(effects)->touchUp(id, time); } }; class MoveResizeFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) AbstractClient *c = workspace()->getMovingClient(); if (!c) { return false; } switch (event->type()) { case QEvent::MouseMove: c->updateMoveResize(event->screenPos().toPoint()); break; case QEvent::MouseButtonRelease: if (event->buttons() == Qt::NoButton) { c->endMoveResize(); } break; default: break; } return true; } bool wheelEvent(QWheelEvent *event) override { Q_UNUSED(event) // filter out while moving a window return workspace()->getMovingClient() != nullptr; } bool keyEvent(QKeyEvent *event) override { AbstractClient *c = workspace()->getMovingClient(); if (!c) { return false; } if (event->type() == QEvent::KeyPress) { c->keyPressEvent(event->key() | event->modifiers()); if (c->isMove() || c->isResize()) { // only update if mode didn't end c->updateMoveResize(input()->globalPointer()); } } return true; } + + bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + AbstractClient *c = workspace()->getMovingClient(); + if (!c) { + return false; + } + return true; + } + + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + AbstractClient *c = workspace()->getMovingClient(); + if (!c) { + return false; + } + if (!m_set) { + m_id = id; + m_set = true; + } + if (m_id == id) { + c->updateMoveResize(pos.toPoint()); + } + return true; + } + + bool touchUp(quint32 id, quint32 time) override { + Q_UNUSED(time) + AbstractClient *c = workspace()->getMovingClient(); + if (!c) { + return false; + } + if (m_id == id || !m_set) { + c->endMoveResize(); + m_set = false; + // pass through to update decoration filter later on + return false; + } + m_set = false; + return true; + } +private: + quint32 m_id = 0; + bool m_set = false; }; class WindowSelectorFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) if (!m_active) { return false; } switch (event->type()) { case QEvent::MouseButtonRelease: if (event->buttons() == Qt::NoButton) { if (event->button() == Qt::RightButton) { cancel(); } else { accept(event->globalPos()); } } break; default: break; } return true; } bool wheelEvent(QWheelEvent *event) override { Q_UNUSED(event) // filter out while selecting a window return m_active; } bool keyEvent(QKeyEvent *event) override { Q_UNUSED(event) if (!m_active) { return false; } waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); passToWaylandServer(event); if (event->type() == QEvent::KeyPress) { // x11 variant does this on key press, so do the same if (event->key() == Qt::Key_Escape) { cancel(); } else if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return || event->key() == Qt::Key_Space) { accept(input()->globalPointer()); } if (input()->supportsPointerWarping()) { int mx = 0; int my = 0; if (event->key() == Qt::Key_Left) { mx = -10; } if (event->key() == Qt::Key_Right) { mx = 10; } if (event->key() == Qt::Key_Up) { my = -10; } if (event->key() == Qt::Key_Down) { my = 10; } if (event->modifiers() & Qt::ControlModifier) { mx /= 10; my /= 10; } input()->warpPointer(input()->globalPointer() + QPointF(mx, my)); } } // filter out while selecting a window return true; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(time) if (!isActive()) { return false; } m_touchPoints.insert(id, pos); return true; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(time) if (!isActive()) { return false; } auto it = m_touchPoints.find(id); if (it != m_touchPoints.end()) { *it = pos; } return true; } bool touchUp(quint32 id, quint32 time) override { Q_UNUSED(time) if (!isActive()) { return false; } auto it = m_touchPoints.find(id); if (it != m_touchPoints.end()) { const auto pos = it.value(); m_touchPoints.erase(it); if (m_touchPoints.isEmpty()) { accept(pos); } } return true; } bool isActive() const { return m_active; } void start(std::function callback) { Q_ASSERT(!m_active); m_active = true; m_callback = callback; input()->keyboard()->update(); input()->cancelTouch(); } void start(std::function callback) { Q_ASSERT(!m_active); m_active = true; m_pointSelectionFallback = callback; input()->keyboard()->update(); input()->cancelTouch(); } private: void deactivate() { m_active = false; m_callback = std::function(); m_pointSelectionFallback = std::function(); input()->pointer()->removeWindowSelectionCursor(); input()->keyboard()->update(); m_touchPoints.clear(); } void cancel() { if (m_callback) { m_callback(nullptr); } if (m_pointSelectionFallback) { m_pointSelectionFallback(QPoint(-1, -1)); } deactivate(); } void accept(const QPoint &pos) { if (m_callback) { // TODO: this ignores shaped windows m_callback(input()->findToplevel(pos)); } if (m_pointSelectionFallback) { m_pointSelectionFallback(pos); } deactivate(); } void accept(const QPointF &pos) { accept(pos.toPoint()); } bool m_active = false; std::function m_callback; std::function m_pointSelectionFallback; QMap m_touchPoints; }; class GlobalShortcutFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton); if (event->type() == QEvent::MouseButtonPress) { if (input()->shortcuts()->processPointerPressed(event->modifiers(), event->buttons())) { return true; } } return false; } bool wheelEvent(QWheelEvent *event) override { if (event->modifiers() == Qt::NoModifier) { return false; } PointerAxisDirection direction = PointerAxisUp; if (event->angleDelta().x() < 0) { direction = PointerAxisRight; } else if (event->angleDelta().x() > 0) { direction = PointerAxisLeft; } else if (event->angleDelta().y() < 0) { direction = PointerAxisDown; } else if (event->angleDelta().y() > 0) { direction = PointerAxisUp; } return input()->shortcuts()->processAxis(event->modifiers(), direction); } bool keyEvent(QKeyEvent *event) override { if (event->type() == QEvent::KeyPress) { return input()->shortcuts()->processKey(static_cast(event)->modifiersRelevantForGlobalShortcuts(), event->key()); } return false; } bool swipeGestureBegin(int fingerCount, quint32 time) override { Q_UNUSED(time) input()->shortcuts()->processSwipeStart(fingerCount); return false; } bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { Q_UNUSED(time) input()->shortcuts()->processSwipeUpdate(delta); return false; } bool swipeGestureCancelled(quint32 time) override { Q_UNUSED(time) input()->shortcuts()->processSwipeCancel(); return false; } bool swipeGestureEnd(quint32 time) override { Q_UNUSED(time) input()->shortcuts()->processSwipeEnd(); return false; } }; namespace { enum class MouseAction { ModifierOnly, ModifierAndWindow }; std::pair performClientMouseAction(QMouseEvent *event, AbstractClient *client, MouseAction action = MouseAction::ModifierOnly) { Options::MouseCommand command = Options::MouseNothing; bool wasAction = false; if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { if (!input()->pointer()->isConstrained() && !workspace()->globalShortcutsDisabled()) { wasAction = true; switch (event->button()) { case Qt::LeftButton: command = options->commandAll1(); break; case Qt::MiddleButton: command = options->commandAll2(); break; case Qt::RightButton: command = options->commandAll3(); break; default: // nothing break; } } } else { if (action == MouseAction::ModifierAndWindow) { command = client->getMouseCommand(event->button(), &wasAction); } } if (wasAction) { return std::make_pair(wasAction, !client->performMouseCommand(command, event->globalPos())); } return std::make_pair(wasAction, false); } std::pair performClientWheelAction(QWheelEvent *event, AbstractClient *c, MouseAction action = MouseAction::ModifierOnly) { bool wasAction = false; Options::MouseCommand command = Options::MouseNothing; if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { if (!input()->pointer()->isConstrained() && !workspace()->globalShortcutsDisabled()) { wasAction = true; command = options->operationWindowMouseWheel(-1 * event->angleDelta().y()); } } else { if (action == MouseAction::ModifierAndWindow) { command = c->getWheelCommand(Qt::Vertical, &wasAction); } } if (wasAction) { return std::make_pair(wasAction, !c->performMouseCommand(command, event->globalPos())); } return std::make_pair(wasAction, false); } } class InternalWindowEventFilter : public InputEventFilter { bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) auto internal = input()->pointer()->internalWindow(); if (!internal) { return false; } - if (event->buttons() == Qt::NoButton) { - // update pointer window only if no button is pressed - input()->pointer()->update(); - } - if (!internal) { - return false; - } // find client switch (event->type()) { case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: { auto s = waylandServer()->findClient(internal); if (s && s->isDecorated()) { // only perform mouse commands on decorated internal windows const auto actionResult = performClientMouseAction(event, s); if (actionResult.first) { return actionResult.second; } } break; } default: break; } QMouseEvent e(event->type(), event->pos() - internal->position(), event->globalPos(), event->button(), event->buttons(), event->modifiers()); e.setAccepted(false); QCoreApplication::sendEvent(internal.data(), &e); return e.isAccepted(); } bool wheelEvent(QWheelEvent *event) override { auto internal = input()->pointer()->internalWindow(); if (!internal) { return false; } if (event->angleDelta().y() != 0) { auto s = waylandServer()->findClient(internal); if (s && s->isDecorated()) { // client window action only on vertical scrolling const auto actionResult = performClientWheelAction(event, s); if (actionResult.first) { return actionResult.second; } } } const QPointF localPos = event->globalPosF() - QPointF(internal->x(), internal->y()); const Qt::Orientation orientation = (event->angleDelta().x() != 0) ? Qt::Horizontal : Qt::Vertical; const int delta = event->angleDelta().x() != 0 ? event->angleDelta().x() : event->angleDelta().y(); QWheelEvent e(localPos, event->globalPosF(), QPoint(), event->angleDelta() * -1, delta * -1, orientation, event->buttons(), event->modifiers()); e.setAccepted(false); QCoreApplication::sendEvent(internal.data(), &e); return e.isAccepted(); } bool keyEvent(QKeyEvent *event) override { const auto &internalClients = waylandServer()->internalClients(); if (internalClients.isEmpty()) { return false; } QWindow *found = nullptr; auto it = internalClients.end(); do { it--; if (QWindow *w = (*it)->internalWindow()) { if (!w->isVisible()) { continue; } if (!screens()->geometry().contains(w->geometry())) { continue; } if (w->property("_q_showWithoutActivating").toBool()) { continue; } if (w->property("outputOnly").toBool()) { continue; } if (w->flags().testFlag(Qt::ToolTip)) { continue; } found = w; break; } } while (it != internalClients.begin()); if (!found) { return false; } auto xkb = input()->keyboard()->xkb(); Qt::Key key = xkb->toQtKey(xkb->toKeysym(event->nativeScanCode())); if (key == Qt::Key_Super_L || key == Qt::Key_Super_R) { // workaround for QTBUG-62102 key = Qt::Key_Meta; } QKeyEvent internalEvent(event->type(), key, event->modifiers(), event->nativeScanCode(), event->nativeVirtualKey(), event->nativeModifiers(), event->text()); internalEvent.setAccepted(false); if (QCoreApplication::sendEvent(found, &internalEvent)) { waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); passToWaylandServer(event); return true; } return false; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { auto seat = waylandServer()->seat(); if (seat->isTouchSequence()) { // something else is getting the events return false; } auto touch = input()->touch(); if (touch->internalPressId() != -1) { - // already on a decoration, ignore further touch points, but filter out + // already on internal window, ignore further touch points, but filter out return true; } // a new touch point seat->setTimestamp(time); - touch->update(pos); auto internal = touch->internalWindow(); if (!internal) { return false; } touch->setInternalPressId(id); // Qt's touch event API is rather complex, let's do fake mouse events instead m_lastGlobalTouchPos = pos; m_lastLocalTouchPos = pos - QPointF(internal->x(), internal->y()); + + QEnterEvent enterEvent(m_lastLocalTouchPos, m_lastLocalTouchPos, pos); + QCoreApplication::sendEvent(internal.data(), &enterEvent); + QMouseEvent e(QEvent::MouseButtonPress, m_lastLocalTouchPos, pos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); e.setAccepted(false); QCoreApplication::sendEvent(internal.data(), &e); return true; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { auto touch = input()->touch(); auto internal = touch->internalWindow(); if (!internal) { return false; } if (touch->internalPressId() == -1) { return false; } waylandServer()->seat()->setTimestamp(time); if (touch->internalPressId() != qint32(id)) { // ignore, but filter out return true; } m_lastGlobalTouchPos = pos; m_lastLocalTouchPos = pos - QPointF(internal->x(), internal->y()); + QMouseEvent e(QEvent::MouseMove, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); QCoreApplication::instance()->sendEvent(internal.data(), &e); return true; } bool touchUp(quint32 id, quint32 time) override { auto touch = input()->touch(); auto internal = touch->internalWindow(); if (!internal) { return false; } if (touch->internalPressId() == -1) { return false; } waylandServer()->seat()->setTimestamp(time); if (touch->internalPressId() != qint32(id)) { // ignore, but filter out return true; } // send mouse up QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); e.setAccepted(false); QCoreApplication::sendEvent(internal.data(), &e); + QEvent leaveEvent(QEvent::Leave); + QCoreApplication::sendEvent(internal.data(), &leaveEvent); + m_lastGlobalTouchPos = QPointF(); m_lastLocalTouchPos = QPointF(); input()->touch()->setInternalPressId(-1); return true; } private: QPointF m_lastGlobalTouchPos; QPointF m_lastLocalTouchPos; }; class DecorationEventFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) auto decoration = input()->pointer()->decoration(); if (!decoration) { return false; } const QPointF p = event->globalPos() - decoration->client()->pos(); switch (event->type()) { case QEvent::MouseMove: { - if (event->buttons() == Qt::NoButton) { - return false; - } QHoverEvent e(QEvent::HoverMove, p, p); QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); decoration->client()->processDecorationMove(p.toPoint(), event->globalPos()); return true; } case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: { const auto actionResult = performClientMouseAction(event, decoration->client()); if (actionResult.first) { return actionResult.second; } QMouseEvent e(event->type(), p, event->globalPos(), event->button(), event->buttons(), event->modifiers()); e.setAccepted(false); QCoreApplication::sendEvent(decoration->decoration(), &e); if (!e.isAccepted() && event->type() == QEvent::MouseButtonPress) { decoration->client()->processDecorationButtonPress(&e); } if (event->type() == QEvent::MouseButtonRelease) { decoration->client()->processDecorationButtonRelease(&e); } return true; } default: break; } return false; } bool wheelEvent(QWheelEvent *event) override { auto decoration = input()->pointer()->decoration(); if (!decoration) { return false; } if (event->angleDelta().y() != 0) { // client window action only on vertical scrolling const auto actionResult = performClientWheelAction(event, decoration->client()); if (actionResult.first) { return actionResult.second; } } const QPointF localPos = event->globalPosF() - decoration->client()->pos(); const Qt::Orientation orientation = (event->angleDelta().x() != 0) ? Qt::Horizontal : Qt::Vertical; const int delta = event->angleDelta().x() != 0 ? event->angleDelta().x() : event->angleDelta().y(); QWheelEvent e(localPos, event->globalPosF(), QPoint(), event->angleDelta(), delta, orientation, event->buttons(), event->modifiers()); e.setAccepted(false); QCoreApplication::sendEvent(decoration.data(), &e); if (e.isAccepted()) { return true; } if ((orientation == Qt::Vertical) && decoration->client()->titlebarPositionUnderMouse()) { decoration->client()->performMouseCommand(options->operationTitlebarMouseWheel(delta * -1), event->globalPosF().toPoint()); } return true; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { auto seat = waylandServer()->seat(); if (seat->isTouchSequence()) { return false; } if (input()->touch()->decorationPressId() != -1) { // already on a decoration, ignore further touch points, but filter out return true; } seat->setTimestamp(time); - input()->touch()->update(pos); auto decoration = input()->touch()->decoration(); if (!decoration) { return false; } + input()->touch()->setDecorationPressId(id); m_lastGlobalTouchPos = pos; m_lastLocalTouchPos = pos - decoration->client()->pos(); + + QHoverEvent hoverEvent(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::sendEvent(decoration->decoration(), &hoverEvent); + QMouseEvent e(QEvent::MouseButtonPress, m_lastLocalTouchPos, pos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); e.setAccepted(false); QCoreApplication::sendEvent(decoration->decoration(), &e); if (!e.isAccepted()) { decoration->client()->processDecorationButtonPress(&e); } return true; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(time) auto decoration = input()->touch()->decoration(); if (!decoration) { return false; } if (input()->touch()->decorationPressId() == -1) { return false; } if (input()->touch()->decorationPressId() != qint32(id)) { // ignore, but filter out return true; } m_lastGlobalTouchPos = pos; m_lastLocalTouchPos = pos - decoration->client()->pos(); - if (auto c = workspace()->getMovingClient()) { - c->updateMoveResize(pos); - } else { - QHoverEvent e(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); - QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); - decoration->client()->processDecorationMove(m_lastLocalTouchPos.toPoint(), pos.toPoint()); - } + + QHoverEvent e(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->client()->processDecorationMove(m_lastLocalTouchPos.toPoint(), pos.toPoint()); return true; } bool touchUp(quint32 id, quint32 time) override { Q_UNUSED(time); auto decoration = input()->touch()->decoration(); if (!decoration) { return false; } if (input()->touch()->decorationPressId() == -1) { return false; } if (input()->touch()->decorationPressId() != qint32(id)) { // ignore, but filter out return true; } + // send mouse up - if (auto c = workspace()->getMovingClient()) { - c->endMoveResize(); - } else { - QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); - e.setAccepted(false); - QCoreApplication::sendEvent(decoration->decoration(), &e); - decoration->client()->processDecorationButtonRelease(&e); - if (input()->pointer()->decoration() == decoration) { - // send motion to current pointer position - const QPointF p = input()->pointer()->pos() - decoration->client()->pos(); - QHoverEvent event(QEvent::HoverMove, p, p); - QCoreApplication::instance()->sendEvent(decoration->decoration(), &event); - } else { - // send leave - QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); - QCoreApplication::instance()->sendEvent(decoration->decoration(), &event); - } - } + QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + decoration->client()->processDecorationButtonRelease(&e); + + QHoverEvent leaveEvent(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::sendEvent(decoration->decoration(), &leaveEvent); m_lastGlobalTouchPos = QPointF(); m_lastLocalTouchPos = QPointF(); input()->touch()->setDecorationPressId(-1); return true; } private: QPointF m_lastGlobalTouchPos; QPointF m_lastLocalTouchPos; }; #ifdef KWIN_BUILD_TABBOX class TabBoxInputFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 button) override { Q_UNUSED(button) if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { return false; } return TabBox::TabBox::self()->handleMouseEvent(event); } bool keyEvent(QKeyEvent *event) override { if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { return false; } auto seat = waylandServer()->seat(); seat->setFocusedKeyboardSurface(nullptr); input()->pointer()->setEnableConstraints(false); // pass the key event to the seat, so that it has a proper model of the currently hold keys // this is important for combinations like alt+shift to ensure that shift is not considered pressed passToWaylandServer(event); if (event->type() == QEvent::KeyPress) { TabBox::TabBox::self()->keyPress(event->modifiers() | event->key()); } else if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == Qt::NoModifier) { TabBox::TabBox::self()->modifiersReleased(); } return true; } bool wheelEvent(QWheelEvent *event) override { if (!TabBox::TabBox::self() || !TabBox::TabBox::self()->isGrabbed()) { return false; } return TabBox::TabBox::self()->handleWheelEvent(event); } }; #endif class ScreenEdgeInputFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) ScreenEdges::self()->isEntered(event); // always forward return false; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(time) // TODO: better check whether a touch sequence is in progress if (m_touchInProgress || waylandServer()->seat()->isTouchSequence()) { // cancel existing touch ScreenEdges::self()->gestureRecognizer()->cancelSwipeGesture(); m_touchInProgress = false; m_id = 0; return false; } if (ScreenEdges::self()->gestureRecognizer()->startSwipeGesture(pos) > 0) { m_touchInProgress = true; m_id = id; m_lastPos = pos; return true; } return false; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(time) if (m_touchInProgress && m_id == id) { ScreenEdges::self()->gestureRecognizer()->updateSwipeGesture(QSizeF(pos.x() - m_lastPos.x(), pos.y() - m_lastPos.y())); m_lastPos = pos; return true; } return false; } bool touchUp(quint32 id, quint32 time) override { Q_UNUSED(time) if (m_touchInProgress && m_id == id) { ScreenEdges::self()->gestureRecognizer()->endSwipeGesture(); m_touchInProgress = false; return true; } return false; } private: bool m_touchInProgress = false; quint32 m_id = 0; QPointF m_lastPos; }; /** * This filter implements window actions. If the event should not be passed to the * current pointer window it will filter out the event **/ class WindowActionInputFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { Q_UNUSED(nativeButton) if (event->type() != QEvent::MouseButtonPress) { return false; } - AbstractClient *c = dynamic_cast(input()->pointer()->window().data()); + AbstractClient *c = dynamic_cast(input()->pointer()->focus().data()); if (!c) { return false; } const auto actionResult = performClientMouseAction(event, c, MouseAction::ModifierAndWindow); if (actionResult.first) { return actionResult.second; } return false; } bool wheelEvent(QWheelEvent *event) override { if (event->angleDelta().y() == 0) { // only actions on vertical scroll return false; } - AbstractClient *c = dynamic_cast(input()->pointer()->window().data()); + AbstractClient *c = dynamic_cast(input()->pointer()->focus().data()); if (!c) { return false; } const auto actionResult = performClientWheelAction(event, c, MouseAction::ModifierAndWindow); if (actionResult.first) { return actionResult.second; } return false; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { Q_UNUSED(id) Q_UNUSED(time) auto seat = waylandServer()->seat(); if (seat->isTouchSequence()) { return false; } - input()->touch()->update(pos); - AbstractClient *c = dynamic_cast(input()->touch()->window().data()); + AbstractClient *c = dynamic_cast(input()->touch()->focus().data()); if (!c) { return false; } bool wasAction = false; const Options::MouseCommand command = c->getMouseCommand(Qt::LeftButton, &wasAction); if (wasAction) { return !c->performMouseCommand(command, pos.toPoint()); } return false; } }; /** * The remaining default input filter which forwards events to other windows **/ class ForwardInputFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { auto seat = waylandServer()->seat(); seat->setTimestamp(event->timestamp()); switch (event->type()) { case QEvent::MouseMove: { - if (event->buttons() == Qt::NoButton) { - // update pointer window only if no button is pressed - input()->pointer()->update(); - input()->pointer()->updatePointerConstraints(); - } seat->setPointerPos(event->globalPos()); MouseEvent *e = static_cast(event); if (e->delta() != QSizeF()) { seat->relativePointerMotion(e->delta(), e->deltaUnaccelerated(), e->timestampMicroseconds()); } break; } case QEvent::MouseButtonPress: seat->pointerButtonPressed(nativeButton); break; case QEvent::MouseButtonRelease: seat->pointerButtonReleased(nativeButton); - if (event->buttons() == Qt::NoButton) { - input()->pointer()->update(); - } break; default: break; } return true; } bool wheelEvent(QWheelEvent *event) override { auto seat = waylandServer()->seat(); seat->setTimestamp(event->timestamp()); const Qt::Orientation orientation = event->angleDelta().x() == 0 ? Qt::Vertical : Qt::Horizontal; seat->pointerAxis(orientation, orientation == Qt::Horizontal ? event->angleDelta().x() : event->angleDelta().y()); return true; } bool keyEvent(QKeyEvent *event) override { if (!workspace()) { return false; } if (event->isAutoRepeat()) { // handled by Wayland client return false; } auto seat = waylandServer()->seat(); input()->keyboard()->update(); seat->setTimestamp(event->timestamp()); passToWaylandServer(event); return true; } bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); - if (!seat->isTouchSequence()) { - input()->touch()->update(pos); - } input()->touch()->insertId(id, seat->touchDown(pos)); return true; } bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); const qint32 kwaylandId = input()->touch()->mappedId(id); if (kwaylandId != -1) { seat->touchMove(kwaylandId, pos); } return true; } bool touchUp(quint32 id, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); const qint32 kwaylandId = input()->touch()->mappedId(id); if (kwaylandId != -1) { seat->touchUp(kwaylandId); input()->touch()->removeId(id); } return true; } bool pinchGestureBegin(int fingerCount, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->startPointerPinchGesture(fingerCount); return true; } bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->updatePointerPinchGesture(delta, scale, angleDelta); return true; } bool pinchGestureEnd(quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->endPointerPinchGesture(); return true; } bool pinchGestureCancelled(quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->cancelPointerPinchGesture(); return true; } bool swipeGestureBegin(int fingerCount, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->startPointerSwipeGesture(fingerCount); return true; } bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->updatePointerSwipeGesture(delta); return true; } bool swipeGestureEnd(quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->endPointerSwipeGesture(); return true; } bool swipeGestureCancelled(quint32 time) override { if (!workspace()) { return false; } auto seat = waylandServer()->seat(); seat->setTimestamp(time); seat->cancelPointerSwipeGesture(); return true; } }; class DragAndDropInputFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { auto seat = waylandServer()->seat(); if (!seat->isDragPointer()) { return false; } seat->setTimestamp(event->timestamp()); switch (event->type()) { case QEvent::MouseMove: { const auto pos = input()->globalPointer(); seat->setPointerPos(pos); - if (Toplevel *t = input()->findToplevel(pos.toPoint())) { + if (Toplevel *t = input()->pointer()->at()) { // TODO: consider decorations if (t->surface() != seat->dragSurface()) { if (AbstractClient *c = qobject_cast(t)) { workspace()->activateClient(c); } seat->setDragTarget(t->surface(), t->inputTransformation()); } } else { // no window at that place, if we have a surface we need to reset seat->setDragTarget(nullptr); } break; } case QEvent::MouseButtonPress: seat->pointerButtonPressed(nativeButton); break; case QEvent::MouseButtonRelease: seat->pointerButtonReleased(nativeButton); break; default: break; } // TODO: should we pass through effects? return true; } }; KWIN_SINGLETON_FACTORY(InputRedirection) static const QString s_touchpadComponent = QStringLiteral("kcm_touchpad"); InputRedirection::InputRedirection(QObject *parent) : QObject(parent) , m_keyboard(new KeyboardInputRedirection(this)) , m_pointer(new PointerInputRedirection(this)) , m_touch(new TouchInputRedirection(this)) , m_shortcuts(new GlobalShortcutsManager(this)) { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); if (Application::usesLibinput()) { if (LogindIntegration::self()->hasSessionControl()) { setupLibInput(); } else { LibInput::Connection::createThread(); if (LogindIntegration::self()->isConnected()) { LogindIntegration::self()->takeControl(); } else { connect(LogindIntegration::self(), &LogindIntegration::connectedChanged, LogindIntegration::self(), &LogindIntegration::takeControl); } connect(LogindIntegration::self(), &LogindIntegration::hasSessionControlChanged, this, [this] (bool sessionControl) { if (sessionControl) { setupLibInput(); } } ); } } connect(kwinApp(), &Application::workspaceCreated, this, &InputRedirection::setupWorkspace); reconfigure(); } InputRedirection::~InputRedirection() { s_self = NULL; qDeleteAll(m_filters); qDeleteAll(m_spies); } void InputRedirection::installInputEventFilter(InputEventFilter *filter) { Q_ASSERT(!m_filters.contains(filter)); m_filters << filter; } void InputRedirection::prependInputEventFilter(InputEventFilter *filter) { Q_ASSERT(!m_filters.contains(filter)); m_filters.prepend(filter); } void InputRedirection::uninstallInputEventFilter(InputEventFilter *filter) { m_filters.removeOne(filter); } void InputRedirection::installInputEventSpy(InputEventSpy *spy) { m_spies << spy; } void InputRedirection::uninstallInputEventSpy(InputEventSpy *spy) { m_spies.removeOne(spy); } void InputRedirection::init() { m_shortcuts->init(); } void InputRedirection::setupWorkspace() { if (waylandServer()) { using namespace KWayland::Server; FakeInputInterface *fakeInput = waylandServer()->display()->createFakeInput(this); fakeInput->create(); connect(fakeInput, &FakeInputInterface::deviceCreated, this, [this] (FakeInputDevice *device) { connect(device, &FakeInputDevice::authenticationRequested, this, [this, device] (const QString &application, const QString &reason) { Q_UNUSED(application) Q_UNUSED(reason) // TODO: make secure device->setAuthentication(true); } ); connect(device, &FakeInputDevice::pointerMotionRequested, this, [this] (const QSizeF &delta) { // TODO: Fix time m_pointer->processMotion(globalPointer() + QPointF(delta.width(), delta.height()), 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::pointerButtonPressRequested, this, [this] (quint32 button) { // TODO: Fix time m_pointer->processButton(button, InputRedirection::PointerButtonPressed, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::pointerButtonReleaseRequested, this, [this] (quint32 button) { // TODO: Fix time m_pointer->processButton(button, InputRedirection::PointerButtonReleased, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::pointerAxisRequested, this, [this] (Qt::Orientation orientation, qreal delta) { // TODO: Fix time InputRedirection::PointerAxis axis; switch (orientation) { case Qt::Horizontal: axis = InputRedirection::PointerAxisHorizontal; break; case Qt::Vertical: axis = InputRedirection::PointerAxisVertical; break; default: Q_UNREACHABLE(); break; } // TODO: Fix time m_pointer->processAxis(axis, delta, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::touchDownRequested, this, [this] (quint32 id, const QPointF &pos) { // TODO: Fix time m_touch->processDown(id, pos, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::touchMotionRequested, this, [this] (quint32 id, const QPointF &pos) { // TODO: Fix time m_touch->processMotion(id, pos, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::touchUpRequested, this, [this] (quint32 id) { // TODO: Fix time m_touch->processUp(id, 0); waylandServer()->simulateUserActivity(); } ); connect(device, &FakeInputDevice::touchCancelRequested, this, [this] () { m_touch->cancel(); } ); connect(device, &FakeInputDevice::touchFrameRequested, this, [this] () { m_touch->frame(); } ); } ); connect(workspace(), &Workspace::configChanged, this, &InputRedirection::reconfigure); m_keyboard->init(); m_pointer->init(); m_touch->init(); } setupInputFilters(); } void InputRedirection::setupInputFilters() { if (LogindIntegration::self()->hasSessionControl()) { installInputEventFilter(new VirtualTerminalFilter); } if (waylandServer()) { installInputEventFilter(new TerminateServerFilter); installInputEventFilter(new DragAndDropInputFilter); installInputEventFilter(new LockScreenFilter); installInputEventFilter(new PopupInputFilter); m_windowSelector = new WindowSelectorFilter; installInputEventFilter(m_windowSelector); } installInputEventFilter(new ScreenEdgeInputFilter); installInputEventFilter(new EffectsFilter); installInputEventFilter(new MoveResizeFilter); #ifdef KWIN_BUILD_TABBOX installInputEventFilter(new TabBoxInputFilter); #endif installInputEventFilter(new GlobalShortcutFilter); - installInputEventFilter(new InternalWindowEventFilter); installInputEventFilter(new DecorationEventFilter); + installInputEventFilter(new InternalWindowEventFilter); if (waylandServer()) { installInputEventFilter(new WindowActionInputFilter); installInputEventFilter(new ForwardInputFilter); } } void InputRedirection::reconfigure() { if (Application::usesLibinput()) { auto inputConfig = kwinApp()->inputConfig(); inputConfig->reparseConfiguration(); const auto config = inputConfig->group(QStringLiteral("keyboard")); const int delay = config.readEntry("RepeatDelay", 660); const int rate = config.readEntry("RepeatRate", 25); const bool enabled = config.readEntry("KeyboardRepeating", 0) == 0; waylandServer()->seat()->setKeyRepeatInfo(enabled ? rate : 0, delay); } } static KWayland::Server::SeatInterface *findSeat() { auto server = waylandServer(); if (!server) { return nullptr; } return server->seat(); } void InputRedirection::setupLibInput() { if (!Application::usesLibinput()) { return; } if (m_libInput) { return; } LibInput::Connection *conn = LibInput::Connection::create(this); m_libInput = conn; if (conn) { if (waylandServer()) { // create relative pointer manager waylandServer()->display()->createRelativePointerManager(KWayland::Server::RelativePointerInterfaceVersion::UnstableV1, waylandServer()->display())->create(); } conn->setInputConfig(kwinApp()->inputConfig()); conn->updateLEDs(m_keyboard->xkb()->leds()); connect(m_keyboard, &KeyboardInputRedirection::ledsChanged, conn, &LibInput::Connection::updateLEDs); connect(conn, &LibInput::Connection::eventsRead, this, [this] { m_libInput->processEvents(); }, Qt::QueuedConnection ); conn->setup(); connect(conn, &LibInput::Connection::pointerButtonChanged, m_pointer, &PointerInputRedirection::processButton); connect(conn, &LibInput::Connection::pointerAxisChanged, m_pointer, &PointerInputRedirection::processAxis); connect(conn, &LibInput::Connection::pinchGestureBegin, m_pointer, &PointerInputRedirection::processPinchGestureBegin); connect(conn, &LibInput::Connection::pinchGestureUpdate, m_pointer, &PointerInputRedirection::processPinchGestureUpdate); connect(conn, &LibInput::Connection::pinchGestureEnd, m_pointer, &PointerInputRedirection::processPinchGestureEnd); connect(conn, &LibInput::Connection::pinchGestureCancelled, m_pointer, &PointerInputRedirection::processPinchGestureCancelled); connect(conn, &LibInput::Connection::swipeGestureBegin, m_pointer, &PointerInputRedirection::processSwipeGestureBegin); connect(conn, &LibInput::Connection::swipeGestureUpdate, m_pointer, &PointerInputRedirection::processSwipeGestureUpdate); connect(conn, &LibInput::Connection::swipeGestureEnd, m_pointer, &PointerInputRedirection::processSwipeGestureEnd); connect(conn, &LibInput::Connection::swipeGestureCancelled, m_pointer, &PointerInputRedirection::processSwipeGestureCancelled); connect(conn, &LibInput::Connection::keyChanged, m_keyboard, &KeyboardInputRedirection::processKey); connect(conn, &LibInput::Connection::pointerMotion, this, [this] (const QSizeF &delta, const QSizeF &deltaNonAccel, uint32_t time, quint64 timeMicroseconds, LibInput::Device *device) { m_pointer->processMotion(m_pointer->pos() + QPointF(delta.width(), delta.height()), delta, deltaNonAccel, time, timeMicroseconds, device); } ); connect(conn, &LibInput::Connection::pointerMotionAbsolute, this, [this] (QPointF orig, QPointF screen, uint32_t time, LibInput::Device *device) { Q_UNUSED(orig) m_pointer->processMotion(screen, time, device); } ); connect(conn, &LibInput::Connection::touchDown, m_touch, &TouchInputRedirection::processDown); connect(conn, &LibInput::Connection::touchUp, m_touch, &TouchInputRedirection::processUp); connect(conn, &LibInput::Connection::touchMotion, m_touch, &TouchInputRedirection::processMotion); connect(conn, &LibInput::Connection::touchCanceled, m_touch, &TouchInputRedirection::cancel); connect(conn, &LibInput::Connection::touchFrame, m_touch, &TouchInputRedirection::frame); auto handleSwitchEvent = [this] (SwitchEvent::State state, quint32 time, quint64 timeMicroseconds, LibInput::Device *device) { SwitchEvent event(state, time, timeMicroseconds, device); processSpies(std::bind(&InputEventSpy::switchEvent, std::placeholders::_1, &event)); processFilters(std::bind(&InputEventFilter::switchEvent, std::placeholders::_1, &event)); }; connect(conn, &LibInput::Connection::switchToggledOn, this, std::bind(handleSwitchEvent, SwitchEvent::State::On, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); connect(conn, &LibInput::Connection::switchToggledOff, this, std::bind(handleSwitchEvent, SwitchEvent::State::Off, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); if (screens()) { setupLibInputWithScreens(); } else { connect(kwinApp(), &Application::screensCreated, this, &InputRedirection::setupLibInputWithScreens); } if (auto s = findSeat()) { // Workaround for QTBUG-54371: if there is no real keyboard Qt doesn't request virtual keyboard s->setHasKeyboard(true); s->setHasPointer(conn->hasPointer()); s->setHasTouch(conn->hasTouch()); connect(conn, &LibInput::Connection::hasAlphaNumericKeyboardChanged, this, [this] (bool set) { if (m_libInput->isSuspended()) { return; } // TODO: this should update the seat, only workaround for QTBUG-54371 emit hasAlphaNumericKeyboardChanged(set); } ); connect(conn, &LibInput::Connection::hasTabletModeSwitchChanged, this, [this] (bool set) { if (m_libInput->isSuspended()) { return; } emit hasTabletModeSwitchChanged(set); } ); connect(conn, &LibInput::Connection::hasPointerChanged, this, [this, s] (bool set) { if (m_libInput->isSuspended()) { return; } s->setHasPointer(set); } ); connect(conn, &LibInput::Connection::hasTouchChanged, this, [this, s] (bool set) { if (m_libInput->isSuspended()) { return; } s->setHasTouch(set); } ); } connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, m_libInput, [this] (bool active) { if (!active) { m_libInput->deactivate(); } } ); } setupTouchpadShortcuts(); } void InputRedirection::setupTouchpadShortcuts() { if (!m_libInput) { return; } QAction *touchpadToggleAction = new QAction(this); QAction *touchpadOnAction = new QAction(this); QAction *touchpadOffAction = new QAction(this); touchpadToggleAction->setObjectName(QStringLiteral("Toggle Touchpad")); touchpadToggleAction->setProperty("componentName", s_touchpadComponent); touchpadOnAction->setObjectName(QStringLiteral("Enable Touchpad")); touchpadOnAction->setProperty("componentName", s_touchpadComponent); touchpadOffAction->setObjectName(QStringLiteral("Disable Touchpad")); touchpadOffAction->setProperty("componentName", s_touchpadComponent); KGlobalAccel::self()->setDefaultShortcut(touchpadToggleAction, QList{Qt::Key_TouchpadToggle}); KGlobalAccel::self()->setShortcut(touchpadToggleAction, QList{Qt::Key_TouchpadToggle}); KGlobalAccel::self()->setDefaultShortcut(touchpadOnAction, QList{Qt::Key_TouchpadOn}); KGlobalAccel::self()->setShortcut(touchpadOnAction, QList{Qt::Key_TouchpadOn}); KGlobalAccel::self()->setDefaultShortcut(touchpadOffAction, QList{Qt::Key_TouchpadOff}); KGlobalAccel::self()->setShortcut(touchpadOffAction, QList{Qt::Key_TouchpadOff}); #ifndef KWIN_BUILD_TESTING registerShortcut(Qt::Key_TouchpadToggle, touchpadToggleAction); registerShortcut(Qt::Key_TouchpadOn, touchpadOnAction); registerShortcut(Qt::Key_TouchpadOff, touchpadOffAction); #endif connect(touchpadToggleAction, &QAction::triggered, m_libInput, &LibInput::Connection::toggleTouchpads); connect(touchpadOnAction, &QAction::triggered, m_libInput, &LibInput::Connection::enableTouchpads); connect(touchpadOffAction, &QAction::triggered, m_libInput, &LibInput::Connection::disableTouchpads); } bool InputRedirection::hasAlphaNumericKeyboard() { if (m_libInput) { return m_libInput->hasAlphaNumericKeyboard(); } return true; } bool InputRedirection::hasTabletModeSwitch() { if (m_libInput) { return m_libInput->hasTabletModeSwitch(); } return false; } void InputRedirection::setupLibInputWithScreens() { if (!screens() || !m_libInput) { return; } m_libInput->setScreenSize(screens()->size()); m_libInput->updateScreens(); connect(screens(), &Screens::sizeChanged, this, [this] { m_libInput->setScreenSize(screens()->size()); } ); connect(screens(), &Screens::changed, m_libInput, &LibInput::Connection::updateScreens); } void InputRedirection::processPointerMotion(const QPointF &pos, uint32_t time) { m_pointer->processMotion(pos, time); } void InputRedirection::processPointerButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time) { m_pointer->processButton(button, state, time); } void InputRedirection::processPointerAxis(InputRedirection::PointerAxis axis, qreal delta, uint32_t time) { m_pointer->processAxis(axis, delta, time); } void InputRedirection::processKeyboardKey(uint32_t key, InputRedirection::KeyboardKeyState state, uint32_t time) { m_keyboard->processKey(key, state, time); } void InputRedirection::processKeyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { m_keyboard->processModifiers(modsDepressed, modsLatched, modsLocked, group); } void InputRedirection::processKeymapChange(int fd, uint32_t size) { m_keyboard->processKeymapChange(fd, size); } void InputRedirection::processTouchDown(qint32 id, const QPointF &pos, quint32 time) { m_touch->processDown(id, pos, time); } void InputRedirection::processTouchUp(qint32 id, quint32 time) { m_touch->processUp(id, time); } void InputRedirection::processTouchMotion(qint32 id, const QPointF &pos, quint32 time) { m_touch->processMotion(id, pos, time); } void InputRedirection::cancelTouch() { m_touch->cancel(); } void InputRedirection::touchFrame() { m_touch->frame(); } Qt::MouseButtons InputRedirection::qtButtonStates() const { return m_pointer->buttons(); } static bool acceptsInput(Toplevel *t, const QPoint &pos) { const QRegion input = t->inputShape(); if (input.isEmpty()) { return true; } return input.translated(t->pos()).contains(pos); } Toplevel *InputRedirection::findToplevel(const QPoint &pos) { if (!Workspace::self()) { return nullptr; } const bool isScreenLocked = waylandServer() && waylandServer()->isScreenLocked(); // TODO: check whether the unmanaged wants input events at all if (!isScreenLocked) { // if an effect overrides the cursor we don't have a window to focus if (effects && static_cast(effects)->isMouseInterception()) { return nullptr; } const UnmanagedList &unmanaged = Workspace::self()->unmanagedList(); foreach (Unmanaged *u, unmanaged) { if (u->geometry().contains(pos) && acceptsInput(u, pos)) { return u; } } } const ToplevelList &stacking = Workspace::self()->stackingOrder(); if (stacking.isEmpty()) { return NULL; } auto it = stacking.end(); do { --it; Toplevel *t = (*it); if (t->isDeleted()) { // a deleted window doesn't get mouse events continue; } if (AbstractClient *c = dynamic_cast(t)) { if (!c->isOnCurrentActivity() || !c->isOnCurrentDesktop() || c->isMinimized() || !c->isCurrentTab() || c->isHiddenInternal()) { continue; } } if (!t->readyForPainting()) { continue; } if (isScreenLocked) { if (!t->isLockScreen() && !t->isInputMethod()) { continue; } } if (t->inputGeometry().contains(pos) && acceptsInput(t, pos)) { return t; } } while (it != stacking.begin()); return NULL; } Qt::KeyboardModifiers InputRedirection::keyboardModifiers() const { return m_keyboard->modifiers(); } Qt::KeyboardModifiers InputRedirection::modifiersRelevantForGlobalShortcuts() const { return m_keyboard->modifiersRelevantForGlobalShortcuts(); } void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action) { Q_UNUSED(shortcut) kwinApp()->platform()->setupActionForGlobalAccel(action); } void InputRedirection::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) { m_shortcuts->registerPointerShortcut(action, modifiers, pointerButtons); } void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) { m_shortcuts->registerAxisShortcut(action, modifiers, axis); } void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) { m_shortcuts->registerTouchpadSwipe(action, direction); } void InputRedirection::registerGlobalAccel(KGlobalAccelInterface *interface) { m_shortcuts->setKGlobalAccelInterface(interface); } void InputRedirection::warpPointer(const QPointF &pos) { m_pointer->warp(pos); } bool InputRedirection::supportsPointerWarping() const { return m_pointer->supportsWarping(); } QPointF InputRedirection::globalPointer() const { return m_pointer->pos(); } void InputRedirection::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) { if (!m_windowSelector || m_windowSelector->isActive()) { callback(nullptr); return; } m_windowSelector->start(callback); m_pointer->setWindowSelectionCursor(cursorName); } void InputRedirection::startInteractivePositionSelection(std::function callback) { if (!m_windowSelector || m_windowSelector->isActive()) { callback(QPoint(-1, -1)); return; } m_windowSelector->start(callback); m_pointer->setWindowSelectionCursor(QByteArray()); } bool InputRedirection::isSelectingWindow() const { return m_windowSelector ? m_windowSelector->isActive() : false; } InputDeviceHandler::InputDeviceHandler(InputRedirection *input) : QObject(input) { } InputDeviceHandler::~InputDeviceHandler() = default; -void InputDeviceHandler::updateDecoration(Toplevel *t, const QPointF &pos) +void InputDeviceHandler::init() { - const auto oldDeco = m_decoration; - bool needsReset = waylandServer()->isScreenLocked(); - if (AbstractClient *c = dynamic_cast(t)) { - // check whether it's on a Decoration - if (c->decoratedClient()) { - const QRect clientRect = QRect(c->clientPos(), c->clientSize()).translated(c->pos()); - if (!clientRect.contains(pos.toPoint())) { - m_decoration = c->decoratedClient(); - } else { - needsReset = true; - } - } else { - needsReset = true; + connect(workspace(), &Workspace::stackingOrderChanged, this, &InputDeviceHandler::update); + connect(workspace(), &Workspace::clientMinimizedChanged, this, &InputDeviceHandler::update); + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, &InputDeviceHandler::update); +} + +bool InputDeviceHandler::setAt(Toplevel *toplevel) +{ + if (m_at == toplevel) { + return false; + } + auto old = m_at; + m_at = toplevel; + emit atChanged(old, toplevel); + return true; +} + +void InputDeviceHandler::setFocus(Toplevel *toplevel) +{ + m_focus.focus = toplevel; + //TODO: call focusUpdate? +} + +void InputDeviceHandler::setDecoration(QPointer decoration) +{ + auto oldDeco = m_focus.decoration; + m_focus.decoration = decoration; + cleanupDecoration(oldDeco.data(), m_focus.decoration.data()); + emit decorationChanged(); +} + +void InputDeviceHandler::setInternalWindow(QWindow *window) +{ + m_focus.internalWindow = window; + //TODO: call internalWindowUpdate? +} + +void InputDeviceHandler::updateFocus() +{ + auto oldFocus = m_focus.focus; + m_focus.focus = m_at; + focusUpdate(oldFocus, m_focus.focus); +} + +bool InputDeviceHandler::updateDecoration() +{ + const auto oldDeco = m_focus.decoration; + m_focus.decoration = nullptr; + + auto *ac = qobject_cast(m_at); + if (ac && ac->decoratedClient()) { + const QRect clientRect = QRect(ac->clientPos(), ac->clientSize()).translated(ac->pos()); + if (!clientRect.contains(position().toPoint())) { + // input device above decoration + m_focus.decoration = ac->decoratedClient(); } - } else { - needsReset = true; } - if (needsReset) { - m_decoration.clear(); + + if (m_focus.decoration == oldDeco) { + // no change to decoration + return false; } + cleanupDecoration(oldDeco.data(), m_focus.decoration.data()); + emit decorationChanged(); + return true; +} - bool leftSend = false; - auto oldWindow = qobject_cast(window().data()); - if (oldWindow && (m_decoration && m_decoration->client() != oldWindow)) { - leftSend = true; - oldWindow->leaveEvent(); +void InputDeviceHandler::updateInternalWindow(QWindow *window) +{ + if (m_focus.internalWindow == window) { + // no change + return; } + const auto oldInternal = m_focus.internalWindow; + m_focus.internalWindow = window; + cleanupInternalWindow(oldInternal, window); +} - if (oldDeco && oldDeco != m_decoration) { - if (oldDeco->client() != t && !leftSend) { - leftSend = true; - oldDeco->client()->leaveEvent(); - } - // send leave - QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); - QCoreApplication::instance()->sendEvent(oldDeco->decoration(), &event); +void InputDeviceHandler::update() +{ + if (!m_inited) { + return; + } + const auto pos = position().toPoint(); + auto internalWindow = findInternalWindow(pos); + + Toplevel *toplevel; + if (internalWindow) { + toplevel = waylandServer()->findClient(internalWindow); + } else { + toplevel = input()->findToplevel(pos); + } + + // Always set the toplevel at the position of the input device. + setAt(toplevel); + + if (focusUpdatesBlocked()) { + return; } - if (m_decoration) { - if (m_decoration->client() != oldWindow) { - m_decoration->client()->enterEvent(pos.toPoint()); - workspace()->updateFocusMousePosition(pos.toPoint()); + + if (internalWindow) { + if (m_focus.internalWindow != internalWindow) { + // changed internal window + updateDecoration(); + updateInternalWindow(internalWindow); + updateFocus(); + } else if (updateDecoration()) { + // went onto or off from decoration, update focus + updateFocus(); } - const QPointF p = pos - t->pos(); - QHoverEvent event(QEvent::HoverMove, p, p); - QCoreApplication::instance()->sendEvent(m_decoration->decoration(), &event); - m_decoration->client()->processDecorationMove(p.toPoint(), pos.toPoint()); + return; + } + updateInternalWindow(nullptr); + + if (m_focus.focus != m_at) { + // focus change + updateDecoration(); + updateFocus(); + return; + } + // check if switched to/from decoration while staying on the same Toplevel + if (updateDecoration()) { + // went onto or off from decoration, update focus + updateFocus(); } } -void InputDeviceHandler::updateInternalWindow(const QPointF &pos) +QWindow* InputDeviceHandler::findInternalWindow(const QPoint &pos) const { - const auto oldInternalWindow = m_internalWindow; - bool found = false; - // TODO: screen locked check without going through wayland server - bool needsReset = waylandServer()->isScreenLocked(); - const auto &internalClients = waylandServer()->internalClients(); - const bool change = m_internalWindow.isNull() || !(m_internalWindow->flags().testFlag(Qt::Popup) && m_internalWindow->isVisible()); - if (!internalClients.isEmpty() && change) { - auto it = internalClients.end(); - do { - it--; - if (QWindow *w = (*it)->internalWindow()) { - if (!w->isVisible()) { - continue; - } - if ((*it)->geometry().contains(pos.toPoint())) { - // check input mask - const QRegion mask = w->mask().translated(w->geometry().topLeft()); - if (!mask.isEmpty() && !mask.contains(pos.toPoint())) { - continue; - } - if (w->property("outputOnly").toBool()) { - continue; - } - m_internalWindow = QPointer(w); - found = true; - break; - } - } - } while (it != internalClients.begin()); - if (!found) { - needsReset = true; - } + if (waylandServer()->isScreenLocked()) { + return nullptr; } - if (needsReset) { - m_internalWindow.clear(); + + const auto &internalClients = waylandServer()->internalClients(); + if (internalClients.isEmpty()) { + return nullptr; } - if (oldInternalWindow != m_internalWindow) { - // changed - if (oldInternalWindow) { - QEvent event(QEvent::Leave); - QCoreApplication::sendEvent(oldInternalWindow.data(), &event); + + auto it = internalClients.end(); + do { + --it; + QWindow *w = (*it)->internalWindow(); + if (!w || !w->isVisible()) { + continue; } - if (m_internalWindow) { - QEnterEvent event(pos - m_internalWindow->position(), - pos - m_internalWindow->position(), - pos); - QCoreApplication::sendEvent(m_internalWindow.data(), &event); + if (!(*it)->geometry().contains(pos)) { + continue; } - emit internalWindowChanged(); - } + // check input mask + const QRegion mask = w->mask().translated(w->geometry().topLeft()); + if (!mask.isEmpty() && !mask.contains(pos)) { + continue; + } + if (w->property("outputOnly").toBool()) { + continue; + } + return w; + } while (it != internalClients.begin()); + + return nullptr; } } // namespace diff --git a/input.h b/input.h index 78f064261..ac4a78562 100644 --- a/input.h +++ b/input.h @@ -1,433 +1,475 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_INPUT_H #define KWIN_INPUT_H #include #include #include #include #include #include #include #include class KGlobalAccelInterface; class QKeySequence; class QMouseEvent; class QKeyEvent; class QWheelEvent; namespace KWin { class GlobalShortcutsManager; class Toplevel; class InputEventFilter; class InputEventSpy; class KeyboardInputRedirection; class PointerInputRedirection; class TouchInputRedirection; class WindowSelectorFilter; class SwitchEvent; namespace Decoration { class DecoratedClientImpl; } namespace LibInput { class Connection; } /** * @brief This class is responsible for redirecting incoming input to the surface which currently * has input or send enter/leave events. * * In addition input is intercepted before passed to the surfaces to have KWin internal areas * getting input first (e.g. screen edges) and filter the input event out if we currently have * a full input grab. * */ class KWIN_EXPORT InputRedirection : public QObject { Q_OBJECT public: enum PointerButtonState { PointerButtonReleased, PointerButtonPressed }; enum PointerAxis { PointerAxisVertical, PointerAxisHorizontal }; enum KeyboardKeyState { KeyboardKeyReleased, KeyboardKeyPressed, KeyboardKeyAutoRepeat }; virtual ~InputRedirection(); void init(); /** * @return const QPointF& The current global pointer position */ QPointF globalPointer() const; Qt::MouseButtons qtButtonStates() const; Qt::KeyboardModifiers keyboardModifiers() const; Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const; void registerShortcut(const QKeySequence &shortcut, QAction *action); /** * @overload * * Like registerShortcut, but also connects QAction::triggered to the @p slot on @p receiver. * It's recommended to use this method as it ensures that the X11 timestamp is updated prior * to the @p slot being invoked. If not using this overload it's required to ensure that * registerShortcut is called before connecting to QAction's triggered signal. **/ template void registerShortcut(const QKeySequence &shortcut, QAction *action, T *receiver, Slot slot); void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action); void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action); void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action); void registerGlobalAccel(KGlobalAccelInterface *interface); /** * @internal */ void processPointerMotion(const QPointF &pos, uint32_t time); /** * @internal */ void processPointerButton(uint32_t button, PointerButtonState state, uint32_t time); /** * @internal */ void processPointerAxis(PointerAxis axis, qreal delta, uint32_t time); /** * @internal */ void processKeyboardKey(uint32_t key, KeyboardKeyState state, uint32_t time); /** * @internal */ void processKeyboardModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); /** * @internal **/ void processKeymapChange(int fd, uint32_t size); void processTouchDown(qint32 id, const QPointF &pos, quint32 time); void processTouchUp(qint32 id, quint32 time); void processTouchMotion(qint32 id, const QPointF &pos, quint32 time); void cancelTouch(); void touchFrame(); bool supportsPointerWarping() const; void warpPointer(const QPointF &pos); /** * Adds the @p filter to the list of event filters and makes it the first * event filter in processing. * * Note: the event filter will get events before the lock screen can get them, thus * this is a security relevant method. **/ void prependInputEventFilter(InputEventFilter *filter); void uninstallInputEventFilter(InputEventFilter *filter); /** * Installs the @p spy for spying on events. **/ void installInputEventSpy(InputEventSpy *spy); /** * Uninstalls the @p spy. This happens automatically when deleting an InputEventSpy. **/ void uninstallInputEventSpy(InputEventSpy *spy); Toplevel *findToplevel(const QPoint &pos); GlobalShortcutsManager *shortcuts() const { return m_shortcuts; } /** * Sends an event through all InputFilters. * The method @p function is invoked on each input filter. Processing is stopped if * a filter returns @c true for @p function. * * The UnaryPredicate is defined like the UnaryPredicate of std::any_of. * The signature of the function should be equivalent to the following: * @code * bool function(const InputEventFilter *spy); * @endcode * * The intended usage is to std::bind the method to invoke on the filter with all arguments * bind. **/ template void processFilters(UnaryPredicate function) { std::any_of(m_filters.constBegin(), m_filters.constEnd(), function); } /** * Sends an event through all input event spies. * The @p function is invoked on each InputEventSpy. * * The UnaryFunction is defined like the UnaryFunction of std::for_each. * The signature of the function should be equivalent to the following: * @code * void function(const InputEventSpy *spy); * @endcode * * The intended usage is to std::bind the method to invoke on the spies with all arguments * bind. **/ template void processSpies(UnaryFunction function) { std::for_each(m_spies.constBegin(), m_spies.constEnd(), function); } KeyboardInputRedirection *keyboard() const { return m_keyboard; } PointerInputRedirection *pointer() const { return m_pointer; } TouchInputRedirection *touch() const { return m_touch; } bool hasAlphaNumericKeyboard(); bool hasTabletModeSwitch(); void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); void startInteractivePositionSelection(std::function callback); bool isSelectingWindow() const; Q_SIGNALS: /** * @brief Emitted when the global pointer position changed * * @param pos The new global pointer position. */ void globalPointerChanged(const QPointF &pos); /** * @brief Emitted when the state of a pointer button changed. * * @param button The button which changed * @param state The new button state */ void pointerButtonStateChanged(uint32_t button, InputRedirection::PointerButtonState state); /** * @brief Emitted when a pointer axis changed * * @param axis The axis on which the even occurred * @param delta The delta of the event. */ void pointerAxisChanged(InputRedirection::PointerAxis axis, qreal delta); /** * @brief Emitted when the modifiers changes. * * Only emitted for the mask which is provided by Qt::KeyboardModifiers, if other modifiers * change signal is not emitted * * @param newMods The new modifiers state * @param oldMods The previous modifiers state */ void keyboardModifiersChanged(Qt::KeyboardModifiers newMods, Qt::KeyboardModifiers oldMods); /** * @brief Emitted when the state of a key changed. * * @param keyCode The keycode of the key which changed * @param oldMods The new key state */ void keyStateChanged(quint32 keyCode, InputRedirection::KeyboardKeyState state); void hasAlphaNumericKeyboardChanged(bool set); void hasTabletModeSwitchChanged(bool set); private: void setupLibInput(); void setupTouchpadShortcuts(); void setupLibInputWithScreens(); void setupWorkspace(); void reconfigure(); void setupInputFilters(); void installInputEventFilter(InputEventFilter *filter); KeyboardInputRedirection *m_keyboard; PointerInputRedirection *m_pointer; TouchInputRedirection *m_touch; GlobalShortcutsManager *m_shortcuts; LibInput::Connection *m_libInput = nullptr; WindowSelectorFilter *m_windowSelector = nullptr; QVector m_filters; QVector m_spies; KWIN_SINGLETON(InputRedirection) friend InputRedirection *input(); friend class DecorationEventFilter; friend class InternalWindowEventFilter; friend class ForwardInputFilter; }; /** * Base class for filtering input events inside InputRedirection. * * The idea behind the InputEventFilter is to have task oriented * filters. E.g. there is one filter taking care of a locked screen, * one to take care of interacting with window decorations, etc. * * A concrete subclass can reimplement the virtual methods and decide * whether an event should be filtered out or not by returning either * @c true or @c false. E.g. the lock screen filter can easily ensure * that all events are filtered out. * * As soon as a filter returns @c true the processing is stopped. If * a filter returns @c false the next one is invoked. This means a filter * installed early gets to see more events than a filter installed later on. * * Deleting an instance of InputEventFilter automatically uninstalls it from * InputRedirection. **/ class KWIN_EXPORT InputEventFilter { public: InputEventFilter(); virtual ~InputEventFilter(); /** * Event filter for pointer events which can be described by a QMouseEvent. * * Please note that the button translation in QMouseEvent cannot cover all * possible buttons. Because of that also the @p nativeButton code is passed * through the filter. For internal areas it's fine to use @p event, but for * passing to client windows the @p nativeButton should be used. * * @param event The event information about the move or button press/release * @param nativeButton The native key code of the button, for move events 0 * @return @c true to stop further event processing, @c false to pass to next filter **/ virtual bool pointerEvent(QMouseEvent *event, quint32 nativeButton); /** * Event filter for pointer axis events. * * @param event The event information about the axis event * @return @c true to stop further event processing, @c false to pass to next filter **/ virtual bool wheelEvent(QWheelEvent *event); /** * Event filter for keyboard events. * * @param event The event information about the key event * @return @c tru to stop further event processing, @c false to pass to next filter. **/ virtual bool keyEvent(QKeyEvent *event); virtual bool touchDown(quint32 id, const QPointF &pos, quint32 time); virtual bool touchMotion(quint32 id, const QPointF &pos, quint32 time); virtual bool touchUp(quint32 id, quint32 time); virtual bool pinchGestureBegin(int fingerCount, quint32 time); virtual bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time); virtual bool pinchGestureEnd(quint32 time); virtual bool pinchGestureCancelled(quint32 time); virtual bool swipeGestureBegin(int fingerCount, quint32 time); virtual bool swipeGestureUpdate(const QSizeF &delta, quint32 time); virtual bool swipeGestureEnd(quint32 time); virtual bool swipeGestureCancelled(quint32 time); virtual bool switchEvent(SwitchEvent *event); protected: void passToWaylandServer(QKeyEvent *event); }; -class InputDeviceHandler : public QObject +class KWIN_EXPORT InputDeviceHandler : public QObject { Q_OBJECT public: virtual ~InputDeviceHandler(); + virtual void init(); - QPointer window() const { - return m_window; + void update(); + + /** + * @brief First Toplevel currently at the position of the input device + * according to the stacking order. + * @return Toplevel* at device position. + */ + QPointer at() const { + return m_at; + } + /** + * @brief Toplevel currently having pointer input focus (this might + * be different from the Toplevel at the position of the pointer). + * @return Toplevel* with pointer focus. + */ + QPointer focus() const { + return m_focus.focus; } + /** + * @brief The Decoration currently receiving events. + * @return decoration with pointer focus. + **/ QPointer decoration() const { - return m_decoration; + return m_focus.decoration; } + /** + * @brief The internal window currently receiving events. + * @return QWindow with pointer focus. + **/ QPointer internalWindow() const { - return m_internalWindow; + return m_focus.internalWindow; } + virtual QPointF position() const = 0; + + void setFocus(Toplevel *toplevel); + void setDecoration(QPointer decoration); + void setInternalWindow(QWindow *window); + Q_SIGNALS: + void atChanged(Toplevel *old, Toplevel *now); void decorationChanged(); - void internalWindowChanged(); protected: explicit InputDeviceHandler(InputRedirection *parent); - void updateDecoration(Toplevel *t, const QPointF &pos); - void updateInternalWindow(const QPointF &pos); - void setWindow(QPointer window = QPointer()) { - m_window = window; + + virtual void cleanupInternalWindow(QWindow *old, QWindow *now) = 0; + virtual void cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) = 0; + + virtual void focusUpdate(Toplevel *old, Toplevel *now) = 0; + + virtual bool focusUpdatesBlocked() { + return false; } - void clearDecoration() { - m_decoration.clear(); + + inline bool inited() const { + return m_inited; } - void clearInternalWindow() { - m_internalWindow.clear(); + inline void setInited(bool set) { + m_inited = set; } private: - /** - * @brief The Toplevel which currently receives events - */ - QPointer m_window; - /** - * @brief The Decoration which currently receives events. - **/ - QPointer m_decoration; - QPointer m_internalWindow; + bool setAt(Toplevel *toplevel); + void updateFocus(); + bool updateDecoration(); + void updateInternalWindow(QWindow *window); + + QWindow* findInternalWindow(const QPoint &pos) const; + + QPointer m_at; + struct { + QPointer focus; + QPointer decoration; + QPointer internalWindow; + } m_focus; + + bool m_inited = false; }; inline InputRedirection *input() { return InputRedirection::s_self; } template inline void InputRedirection::registerShortcut(const QKeySequence &shortcut, QAction *action, T *receiver, Slot slot) { registerShortcut(shortcut, action); connect(action, &QAction::triggered, receiver, slot); } } // namespace KWin Q_DECLARE_METATYPE(KWin::InputRedirection::KeyboardKeyState) Q_DECLARE_METATYPE(KWin::InputRedirection::PointerButtonState) Q_DECLARE_METATYPE(KWin::InputRedirection::PointerAxis) #endif // KWIN_INPUT_H diff --git a/pointer_input.cpp b/pointer_input.cpp index 00fc376f5..eb1b203f8 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -1,1369 +1,1411 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "pointer_input.h" #include "platform.h" #include "client.h" #include "effects.h" #include "input_event.h" #include "input_event_spy.h" #include "osd.h" #include "screens.h" #include "shell_client.h" #include "wayland_cursor_theme.h" #include "wayland_server.h" #include "workspace.h" #include "decorations/decoratedclient.h" #include "screens.h" // KDecoration #include // KWayland #include #include #include #include #include #include #include #include // screenlocker #include #include #include #include // Wayland #include #include namespace KWin { static Qt::MouseButton buttonToQtMouseButton(uint32_t button) { switch (button) { case BTN_LEFT: return Qt::LeftButton; case BTN_MIDDLE: return Qt::MiddleButton; case BTN_RIGHT: return Qt::RightButton; case BTN_SIDE: // in QtWayland mapped like that return Qt::ExtraButton1; case BTN_EXTRA: // in QtWayland mapped like that return Qt::ExtraButton2; case BTN_BACK: return Qt::BackButton; case BTN_FORWARD: return Qt::ForwardButton; case BTN_TASK: return Qt::TaskButton; // mapped like that in QtWayland case 0x118: return Qt::ExtraButton6; case 0x119: return Qt::ExtraButton7; case 0x11a: return Qt::ExtraButton8; case 0x11b: return Qt::ExtraButton9; case 0x11c: return Qt::ExtraButton10; case 0x11d: return Qt::ExtraButton11; case 0x11e: return Qt::ExtraButton12; case 0x11f: return Qt::ExtraButton13; } // all other values get mapped to ExtraButton24 // this is actually incorrect but doesn't matter in our usage // KWin internally doesn't use these high extra buttons anyway // it's only needed for recognizing whether buttons are pressed // if multiple buttons are mapped to the value the evaluation whether // buttons are pressed is correct and that's all we care about. return Qt::ExtraButton24; } static bool screenContainsPos(const QPointF &pos) { for (int i = 0; i < screens()->count(); ++i) { if (screens()->geometry(i).contains(pos.toPoint())) { return true; } } return false; } static QPointF confineToBoundingBox(const QPointF &pos, const QRectF &boundingBox) { return QPointF( qBound(boundingBox.left(), pos.x(), boundingBox.right() - 1.0), qBound(boundingBox.top(), pos.y(), boundingBox.bottom() - 1.0) ); } PointerInputRedirection::PointerInputRedirection(InputRedirection* parent) : InputDeviceHandler(parent) , m_cursor(nullptr) , m_supportsWarping(Application::usesLibinput()) { } PointerInputRedirection::~PointerInputRedirection() = default; void PointerInputRedirection::init() { - Q_ASSERT(!m_inited); + Q_ASSERT(!inited()); m_cursor = new CursorImage(this); - m_inited = true; + setInited(true); + InputDeviceHandler::init(); + connect(m_cursor, &CursorImage::changed, kwinApp()->platform(), &Platform::cursorChanged); emit m_cursor->changed(); - connect(workspace(), &Workspace::stackingOrderChanged, this, &PointerInputRedirection::update); - connect(workspace(), &Workspace::clientMinimizedChanged, this, &PointerInputRedirection::update); + connect(screens(), &Screens::changed, this, &PointerInputRedirection::updateAfterScreenChange); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { waylandServer()->seat()->cancelPointerPinchGesture(); waylandServer()->seat()->cancelPointerSwipeGesture(); update(); } ); } - connect(workspace(), &QObject::destroyed, this, [this] { m_inited = false; }); - connect(waylandServer(), &QObject::destroyed, this, [this] { m_inited = false; }); + connect(workspace(), &QObject::destroyed, this, [this] { setInited(false); }); + connect(waylandServer(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { // need to force a focused pointer change waylandServer()->seat()->setFocusedPointerSurface(nullptr); - setWindow(); + setFocus(nullptr); update(); } ); - connect(this, &PointerInputRedirection::internalWindowChanged, this, - [this] { - disconnect(m_internalWindowConnection); - m_internalWindowConnection = QMetaObject::Connection(); - if (internalWindow()) { - m_internalWindowConnection = connect(internalWindow().data(), &QWindow::visibleChanged, this, - [this] (bool visible) { - if (!visible) { - update(); - } - } - ); - } - } - ); - connect(this, &PointerInputRedirection::decorationChanged, this, - [this] { - disconnect(m_decorationGeometryConnection); - m_decorationGeometryConnection = QMetaObject::Connection(); - if (decoration()) { - m_decorationGeometryConnection = connect(decoration()->client(), &AbstractClient::geometryChanged, this, - [this] { - // ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140 - const auto oldDeco = decoration(); - update(); - if (oldDeco && oldDeco == decoration() && !decoration()->client()->isMove() && !decoration()->client()->isResize() && !areButtonsPressed()) { - // position of window did not change, we need to send HoverMotion manually - const QPointF p = m_pos - decoration()->client()->pos(); - QHoverEvent event(QEvent::HoverMove, p, p); - QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); - } - }, Qt::QueuedConnection); - } - } - ); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::clientStartUserMovedResized, this, &PointerInputRedirection::updateOnStartMoveResize); connect(c, &AbstractClient::clientFinishUserMovedResized, this, &PointerInputRedirection::update); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); // warp the cursor to center of screen warp(screens()->geometry().center()); updateAfterScreenChange(); } void PointerInputRedirection::updateOnStartMoveResize() { - breakPointerConstraints(window() ? window()->surface() : nullptr); + breakPointerConstraints(focus() ? focus()->surface() : nullptr); disconnectPointerConstraintsConnection(); - setWindow(); + setFocus(nullptr); waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::updateToReset() { if (internalWindow()) { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); QEvent event(QEvent::Leave); QCoreApplication::sendEvent(internalWindow().data(), &event); - clearInternalWindow(); + setInternalWindow(nullptr); } if (decoration()) { QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); - clearDecoration(); + setDecoration(nullptr); } - if (window()) { - if (AbstractClient *c = qobject_cast(window().data())) { + if (focus()) { + if (AbstractClient *c = qobject_cast(focus().data())) { c->leaveEvent(); } - disconnect(m_windowGeometryConnection); - m_windowGeometryConnection = QMetaObject::Connection(); - breakPointerConstraints(window()->surface()); + disconnect(m_focusGeometryConnection); + m_focusGeometryConnection = QMetaObject::Connection(); + breakPointerConstraints(focus()->surface()); disconnectPointerConstraintsConnection(); - setWindow(); + setFocus(nullptr); } waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device) { processMotion(pos, QSizeF(), QSizeF(), time, 0, device); } class PositionUpdateBlocker { public: PositionUpdateBlocker(PointerInputRedirection *pointer) : m_pointer(pointer) { s_counter++; } ~PositionUpdateBlocker() { s_counter--; if (s_counter == 0) { if (!s_scheduledPositions.isEmpty()) { const auto pos = s_scheduledPositions.takeFirst(); m_pointer->processMotion(pos.pos, pos.delta, pos.deltaNonAccelerated, pos.time, pos.timeUsec, nullptr); } } } static bool isPositionBlocked() { return s_counter > 0; } static void schedulePosition(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec) { s_scheduledPositions.append({pos, delta, deltaNonAccelerated, time, timeUsec}); } private: static int s_counter; struct ScheduledPosition { QPointF pos; QSizeF delta; QSizeF deltaNonAccelerated; quint32 time; quint64 timeUsec; }; static QVector s_scheduledPositions; PointerInputRedirection *m_pointer; }; int PositionUpdateBlocker::s_counter = 0; QVector PositionUpdateBlocker::s_scheduledPositions; void PointerInputRedirection::processMotion(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec, LibInput::Device *device) { - if (!m_inited) { + if (!inited()) { return; } if (PositionUpdateBlocker::isPositionBlocked()) { PositionUpdateBlocker::schedulePosition(pos, delta, deltaNonAccelerated, time, timeUsec); return; } PositionUpdateBlocker blocker(this); updatePosition(pos); MouseEvent event(QEvent::MouseMove, m_pos, Qt::NoButton, m_qtButtons, input()->keyboardModifiers(), time, delta, deltaNonAccelerated, timeUsec, device); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); + update(); input()->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); input()->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, 0)); } void PointerInputRedirection::processButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time, LibInput::Device *device) { - updateButton(button, state); - QEvent::Type type; switch (state) { case InputRedirection::PointerButtonReleased: type = QEvent::MouseButtonRelease; break; case InputRedirection::PointerButtonPressed: type = QEvent::MouseButtonPress; + update(); break; default: Q_UNREACHABLE(); return; } + updateButton(button, state); + MouseEvent event(type, m_pos, buttonToQtMouseButton(button), m_qtButtons, input()->keyboardModifiers(), time, QSizeF(), QSizeF(), 0, device); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); event.setNativeButton(button); input()->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); - if (!m_inited) { + if (!inited()) { return; } input()->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, button)); + + if (state == InputRedirection::PointerButtonReleased) { + update(); + } } void PointerInputRedirection::processAxis(InputRedirection::PointerAxis axis, qreal delta, uint32_t time, LibInput::Device *device) { if (delta == 0) { return; } + update(); emit input()->pointerAxisChanged(axis, delta); WheelEvent wheelEvent(m_pos, delta, (axis == InputRedirection::PointerAxisHorizontal) ? Qt::Horizontal : Qt::Vertical, m_qtButtons, input()->keyboardModifiers(), time, device); wheelEvent.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); input()->processSpies(std::bind(&InputEventSpy::wheelEvent, std::placeholders::_1, &wheelEvent)); - if (!m_inited) { + if (!inited()) { return; } input()->processFilters(std::bind(&InputEventFilter::wheelEvent, std::placeholders::_1, &wheelEvent)); } void PointerInputRedirection::processSwipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } input()->processSpies(std::bind(&InputEventSpy::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processSwipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureUpdate, std::placeholders::_1, delta, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureUpdate, std::placeholders::_1, delta, time)); } void PointerInputRedirection::processSwipeGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processSwipeGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); } void PointerInputRedirection::processPinchGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureCancelled, std::placeholders::_1, time)); } bool PointerInputRedirection::areButtonsPressed() const { for (auto state : m_buttons) { if (state == InputRedirection::PointerButtonPressed) { return true; } } return false; } -static bool s_cursorUpdateBlocking = false; - -void PointerInputRedirection::update() +bool PointerInputRedirection::focusUpdatesBlocked() { - if (!m_inited) { - return; + if (!inited()) { + return true; } if (waylandServer()->seat()->isDragPointer()) { // ignore during drag and drop - return; + return true; + } + if (waylandServer()->seat()->isTouchSequence()) { + // ignore during touch operations + return true; } if (input()->isSelectingWindow()) { - return; + return true; } if (areButtonsPressed()) { - return; + return true; } - Toplevel *t = input()->findToplevel(m_pos.toPoint()); - const auto oldDeco = decoration(); - updateInternalWindow(m_pos); - if (!internalWindow()) { - updateDecoration(t, m_pos); - } else { - updateDecoration(waylandServer()->findClient(internalWindow()), m_pos); - if (decoration()) { - disconnect(m_internalWindowConnection); - m_internalWindowConnection = QMetaObject::Connection(); - QEvent event(QEvent::Leave); - QCoreApplication::sendEvent(internalWindow().data(), &event); - clearInternalWindow(); - } + return false; +} + +void PointerInputRedirection::cleanupInternalWindow(QWindow *old, QWindow *now) +{ + disconnect(m_internalWindowConnection); + m_internalWindowConnection = QMetaObject::Connection(); + + if (old) { + // leave internal window + // TODO: do this instead via Wayland protocol as below + QEvent leaveEvent(QEvent::Leave); + QCoreApplication::sendEvent(old, &leaveEvent); } - if (decoration() || internalWindow()) { - t = nullptr; + + if (now) { + m_internalWindowConnection = connect(internalWindow().data(), &QWindow::visibleChanged, this, + [this] (bool visible) { + if (!visible) { + update(); + } + } + ); } - if (decoration() != oldDeco) { - emit decorationChanged(); +} + +void PointerInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) +{ + disconnect(m_decorationGeometryConnection); + m_decorationGeometryConnection = QMetaObject::Connection(); + workspace()->updateFocusMousePosition(position().toPoint()); + + if (old) { + // send leave event to old decoration + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(old->decoration(), &event); } - auto oldWindow = window(); - if (!oldWindow.isNull() && t == window().data()) { + if (!now) { + // left decoration return; } - auto seat = waylandServer()->seat(); - // disconnect old surface - if (oldWindow) { - if (AbstractClient *c = qobject_cast(oldWindow.data())) { - c->leaveEvent(); - } - disconnect(m_windowGeometryConnection); - m_windowGeometryConnection = QMetaObject::Connection(); - breakPointerConstraints(oldWindow->surface()); + + waylandServer()->seat()->setFocusedPointerSurface(nullptr); + + auto pos = m_pos - now->client()->pos(); + QHoverEvent event(QEvent::HoverEnter, pos, pos); + QCoreApplication::instance()->sendEvent(now->decoration(), &event); + now->client()->processDecorationMove(pos.toPoint(), m_pos.toPoint()); + + m_decorationGeometryConnection = connect(decoration()->client(), &AbstractClient::geometryChanged, this, + [this] { + // ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140 + const auto oldDeco = decoration(); + update(); + if (oldDeco && + oldDeco == decoration() && + !decoration()->client()->isMove() && + !decoration()->client()->isResize() && + !areButtonsPressed()) { + // position of window did not change, we need to send HoverMotion manually + const QPointF p = m_pos - decoration()->client()->pos(); + QHoverEvent event(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); + } + }, Qt::QueuedConnection); +} + +static bool s_cursorUpdateBlocking = false; + +void PointerInputRedirection::focusUpdate(Toplevel *focusOld, Toplevel *focusNow) +{ + if (AbstractClient *ac = qobject_cast(focusOld)) { + ac->leaveEvent(); + breakPointerConstraints(ac->surface()); disconnectPointerConstraintsConnection(); } - if (AbstractClient *c = qobject_cast(t)) { - // only send enter if it wasn't on deco for the same client before - if (decoration().isNull() || decoration()->client() != c) { - c->enterEvent(m_pos.toPoint()); - workspace()->updateFocusMousePosition(m_pos.toPoint()); - } + disconnect(m_focusGeometryConnection); + m_focusGeometryConnection = QMetaObject::Connection(); + + if (AbstractClient *ac = qobject_cast(focusNow)) { + ac->enterEvent(m_pos.toPoint()); + workspace()->updateFocusMousePosition(m_pos.toPoint()); } - if (t && t->surface()) { - setWindow(t); - // TODO: add convenient API to update global pos together with updating focused surface - warpXcbOnSurfaceLeft(t->surface()); - s_cursorUpdateBlocking = true; - seat->setFocusedPointerSurface(nullptr); - s_cursorUpdateBlocking = false; - seat->setPointerPos(m_pos.toPoint()); - seat->setFocusedPointerSurface(t->surface(), t->inputTransformation()); - m_windowGeometryConnection = connect(t, &Toplevel::geometryChanged, this, - [this] { - if (window().isNull()) { - return; - } - // TODO: can we check on the client instead? - if (workspace()->getMovingClient()) { - // don't update while moving - return; - } - auto seat = waylandServer()->seat(); - if (window().data()->surface() != seat->focusedPointerSurface()) { - return; - } - seat->setFocusedPointerSurfaceTransformation(window().data()->inputTransformation()); - } - ); - m_constraintsConnection = connect(window()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged, - this, &PointerInputRedirection::updatePointerConstraints); - m_constraintsActivatedConnection = connect(workspace(), &Workspace::clientActivated, - this, &PointerInputRedirection::updatePointerConstraints); - // check whether a pointer confinement/lock fires - updatePointerConstraints(); - } else { - setWindow(); + + auto seat = waylandServer()->seat(); + if (!focusNow || !focusNow->surface() || decoration()) { + // no new surface or internal window or on decoration -> cleanup warpXcbOnSurfaceLeft(nullptr); seat->setFocusedPointerSurface(nullptr); - t = nullptr; + return; } + + if (internalWindow()) { + // enter internal window + // TODO: do this instead via Wayland protocol as below + const auto pos = at()->pos(); + QEnterEvent enterEvent(pos, pos, m_pos); + QCoreApplication::sendEvent(internalWindow().data(), &enterEvent); + } + + // TODO: add convenient API to update global pos together with updating focused surface + warpXcbOnSurfaceLeft(focusNow->surface()); + + // TODO: why? in order to reset the cursor icon? + s_cursorUpdateBlocking = true; + seat->setFocusedPointerSurface(nullptr); + s_cursorUpdateBlocking = false; + + seat->setPointerPos(m_pos.toPoint()); + seat->setFocusedPointerSurface(focusNow->surface(), focusNow->inputTransformation()); + + m_focusGeometryConnection = connect(focusNow, &Toplevel::geometryChanged, this, + [this] { + // TODO: why no assert possible? + if (!focus()) { + return; + } + // TODO: can we check on the client instead? + if (workspace()->getMovingClient()) { + // don't update while moving + return; + } + auto seat = waylandServer()->seat(); + if (focus()->surface() != seat->focusedPointerSurface()) { + return; + } + seat->setFocusedPointerSurfaceTransformation(focus()->inputTransformation()); + } + ); + + m_constraintsConnection = connect(focusNow->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged, + this, &PointerInputRedirection::updatePointerConstraints); + m_constraintsActivatedConnection = connect(workspace(), &Workspace::clientActivated, + this, &PointerInputRedirection::updatePointerConstraints); + updatePointerConstraints(); } void PointerInputRedirection::breakPointerConstraints(KWayland::Server::SurfaceInterface *surface) { // cancel pointer constraints if (surface) { auto c = surface->confinedPointer(); if (c && c->isConfined()) { c->setConfined(false); } auto l = surface->lockedPointer(); if (l && l->isLocked()) { l->setLocked(false); } } disconnectConfinedPointerRegionConnection(); m_confined = false; m_locked = false; } void PointerInputRedirection::disconnectConfinedPointerRegionConnection() { disconnect(m_confinedPointerRegionConnection); m_confinedPointerRegionConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectLockedPointerAboutToBeUnboundConnection() { disconnect(m_lockedPointerAboutToBeUnboundConnection); m_lockedPointerAboutToBeUnboundConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectPointerConstraintsConnection() { disconnect(m_constraintsConnection); m_constraintsConnection = QMetaObject::Connection(); disconnect(m_constraintsActivatedConnection); m_constraintsActivatedConnection = QMetaObject::Connection(); } template static QRegion getConstraintRegion(Toplevel *t, T *constraint) { const QRegion windowShape = t->inputShape(); const QRegion windowRegion = windowShape.isEmpty() ? QRegion(0, 0, t->clientSize().width(), t->clientSize().height()) : windowShape; const QRegion intersected = constraint->region().isEmpty() ? windowRegion : windowRegion.intersected(constraint->region()); return intersected.translated(t->pos() + t->clientPos()); } void PointerInputRedirection::setEnableConstraints(bool set) { if (m_enableConstraints == set) { return; } m_enableConstraints = set; updatePointerConstraints(); } void PointerInputRedirection::updatePointerConstraints() { - if (window().isNull()) { + if (focus().isNull()) { return; } - const auto s = window()->surface(); + const auto s = focus()->surface(); if (!s) { return; } if (s != waylandServer()->seat()->focusedPointerSurface()) { return; } if (!supportsWarping()) { return; } - const bool canConstrain = m_enableConstraints && window() == workspace()->activeClient(); + const bool canConstrain = m_enableConstraints && focus() == workspace()->activeClient(); const auto cf = s->confinedPointer(); if (cf) { if (cf->isConfined()) { if (!canConstrain) { cf->setConfined(false); m_confined = false; disconnectConfinedPointerRegionConnection(); } return; } - const QRegion r = getConstraintRegion(window().data(), cf.data()); + const QRegion r = getConstraintRegion(focus().data(), cf.data()); if (canConstrain && r.contains(m_pos.toPoint())) { cf->setConfined(true); m_confined = true; m_confinedPointerRegionConnection = connect(cf.data(), &KWayland::Server::ConfinedPointerInterface::regionChanged, this, [this] { - if (!window()) { + if (!focus()) { return; } - const auto s = window()->surface(); + const auto s = focus()->surface(); if (!s) { return; } const auto cf = s->confinedPointer(); - if (!getConstraintRegion(window().data(), cf.data()).contains(m_pos.toPoint())) { + if (!getConstraintRegion(focus().data(), cf.data()).contains(m_pos.toPoint())) { // pointer no longer in confined region, break the confinement cf->setConfined(false); m_confined = false; } else { if (!cf->isConfined()) { cf->setConfined(true); m_confined = true; } } } ); return; } } else { m_confined = false; disconnectConfinedPointerRegionConnection(); } const auto lock = s->lockedPointer(); if (lock) { if (lock->isLocked()) { if (!canConstrain) { const auto hint = lock->cursorPositionHint(); lock->setLocked(false); m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); - if (! (hint.x() < 0 || hint.y() < 0) && window()) { - processMotion(window()->pos() - window()->clientContentPos() + hint, waylandServer()->seat()->timestamp()); + if (! (hint.x() < 0 || hint.y() < 0) && focus()) { + processMotion(focus()->pos() - focus()->clientContentPos() + hint, waylandServer()->seat()->timestamp()); } } return; } - const QRegion r = getConstraintRegion(window().data(), lock.data()); + const QRegion r = getConstraintRegion(focus().data(), lock.data()); if (canConstrain && r.contains(m_pos.toPoint())) { lock->setLocked(true); m_locked = true; // The client might cancel pointer locking from its side by unbinding the LockedPointerInterface. // In this case the cached cursor position hint must be fetched before the resource goes away m_lockedPointerAboutToBeUnboundConnection = connect(lock.data(), &KWayland::Server::LockedPointerInterface::aboutToBeUnbound, this, [this, lock]() { const auto hint = lock->cursorPositionHint(); - if (hint.x() < 0 || hint.y() < 0 || !window()) { + if (hint.x() < 0 || hint.y() < 0 || !focus()) { return; } - auto globalHint = window()->pos() - window()->clientContentPos() + hint; + auto globalHint = focus()->pos() - focus()->clientContentPos() + hint; // When the resource finally goes away, reposition the cursor according to the hint connect(lock.data(), &KWayland::Server::LockedPointerInterface::unbound, this, [this, globalHint]() { processMotion(globalHint, waylandServer()->seat()->timestamp()); }); } ); // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region } } else { m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); } } void PointerInputRedirection::warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *newSurface) { auto xc = waylandServer()->xWaylandConnection(); if (!xc) { // No XWayland, no point in warping the x cursor return; } const auto c = kwinApp()->x11Connection(); if (!c) { return; } static bool s_hasXWayland119 = xcb_get_setup(c)->release_number >= 11900000; if (s_hasXWayland119) { return; } if (newSurface && newSurface->client() == xc) { // new window is an X window return; } auto s = waylandServer()->seat()->focusedPointerSurface(); if (!s || s->client() != xc) { // pointer was not on an X window return; } // warp pointer to 0/0 to trigger leave events on previously focused X window xcb_warp_pointer(c, XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 0, 0), xcb_flush(c); } QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const { - if (!window()) { + if (!focus()) { return pos; } - auto s = window()->surface(); + auto s = focus()->surface(); if (!s) { return pos; } auto cf = s->confinedPointer(); if (!cf) { return pos; } if (!cf->isConfined()) { return pos; } - const QRegion confinementRegion = getConstraintRegion(window().data(), cf.data()); + const QRegion confinementRegion = getConstraintRegion(focus().data(), cf.data()); if (confinementRegion.contains(pos.toPoint())) { return pos; } QPointF p = pos; // allow either x or y to pass p = QPointF(m_pos.x(), pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } p = QPointF(pos.x(), m_pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } return m_pos; } void PointerInputRedirection::updatePosition(const QPointF &pos) { if (m_locked) { // locked pointer should not move return; } // verify that at least one screen contains the pointer position QPointF p = pos; if (!screenContainsPos(p)) { const QRectF unitedScreensGeometry = screens()->geometry(); p = confineToBoundingBox(p, unitedScreensGeometry); if (!screenContainsPos(p)) { const QRectF currentScreenGeometry = screens()->geometry(screens()->number(m_pos.toPoint())); p = confineToBoundingBox(p, currentScreenGeometry); } } p = applyPointerConfinement(p); if (p == m_pos) { // didn't change due to confinement return; } // verify screen confinement if (!screenContainsPos(p)) { return; } m_pos = p; emit input()->globalPointerChanged(m_pos); } void PointerInputRedirection::updateButton(uint32_t button, InputRedirection::PointerButtonState state) { m_buttons[button] = state; // update Qt buttons m_qtButtons = Qt::NoButton; for (auto it = m_buttons.constBegin(); it != m_buttons.constEnd(); ++it) { if (it.value() == InputRedirection::PointerButtonReleased) { continue; } m_qtButtons |= buttonToQtMouseButton(it.key()); } emit input()->pointerButtonStateChanged(button, state); } void PointerInputRedirection::warp(const QPointF &pos) { if (supportsWarping()) { kwinApp()->platform()->warpPointer(pos); processMotion(pos, waylandServer()->seat()->timestamp()); } } bool PointerInputRedirection::supportsWarping() const { - if (!m_inited) { + if (!inited()) { return false; } if (m_supportsWarping) { return true; } if (kwinApp()->platform()->supportsPointerWarping()) { return true; } return false; } void PointerInputRedirection::updateAfterScreenChange() { - if (!m_inited) { + if (!inited()) { return; } if (screenContainsPos(m_pos)) { // pointer still on a screen return; } // pointer no longer on a screen, reposition to closes screen const QPointF pos = screens()->geometry(screens()->number(m_pos.toPoint())).center(); // TODO: better way to get timestamps processMotion(pos, waylandServer()->seat()->timestamp()); } QImage PointerInputRedirection::cursorImage() const { - if (!m_inited) { + if (!inited()) { return QImage(); } return m_cursor->image(); } QPoint PointerInputRedirection::cursorHotSpot() const { - if (!m_inited) { + if (!inited()) { return QPoint(); } return m_cursor->hotSpot(); } void PointerInputRedirection::markCursorAsRendered() { - if (!m_inited) { + if (!inited()) { return; } m_cursor->markAsRendered(); } +QPointF PointerInputRedirection::position() const +{ + return m_pos.toPoint(); +} + void PointerInputRedirection::setEffectsOverrideCursor(Qt::CursorShape shape) { - if (!m_inited) { + if (!inited()) { return; } // current pointer focus window should get a leave event update(); m_cursor->setEffectsOverrideCursor(shape); } void PointerInputRedirection::removeEffectsOverrideCursor() { - if (!m_inited) { + if (!inited()) { return; } // cursor position might have changed while there was an effect in place update(); m_cursor->removeEffectsOverrideCursor(); } void PointerInputRedirection::setWindowSelectionCursor(const QByteArray &shape) { - if (!m_inited) { + if (!inited()) { return; } // send leave to current pointer focus window updateToReset(); m_cursor->setWindowSelectionCursor(shape); } void PointerInputRedirection::removeWindowSelectionCursor() { - if (!m_inited) { + if (!inited()) { return; } update(); m_cursor->removeWindowSelectionCursor(); } CursorImage::CursorImage(PointerInputRedirection *parent) : QObject(parent) , m_pointer(parent) { connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::focusedPointerChanged, this, &CursorImage::update); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragStarted, this, &CursorImage::updateDrag); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { disconnect(m_drag.connection); reevaluteSource(); } ); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &CursorImage::reevaluteSource); } connect(m_pointer, &PointerInputRedirection::decorationChanged, this, &CursorImage::updateDecoration); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::moveResizedChanged, this, &CursorImage::updateMoveResize); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); if (m_cursorTheme) { connect(m_cursorTheme, &WaylandCursorTheme::themeChanged, this, [this] { m_cursors.clear(); m_cursorsByName.clear(); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); updateDecorationCursor(); updateMoveResize(); // TODO: update effects } ); } m_surfaceRenderedTimer.start(); } CursorImage::~CursorImage() = default; void CursorImage::markAsRendered() { if (m_currentSource == CursorSource::DragAndDrop) { // always sending a frame rendered to the drag icon surface to not freeze QtWayland (see https://bugreports.qt.io/browse/QTBUG-51599 ) if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto s = ddi->icon()) { s->frameRendered(m_surfaceRenderedTimer.elapsed()); } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); return; } if (m_currentSource != CursorSource::LockScreen && m_currentSource != CursorSource::PointerSurface) { return; } auto p = waylandServer()->seat()->focusedPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); } void CursorImage::update() { if (s_cursorUpdateBlocking) { return; } using namespace KWayland::Server; disconnect(m_serverCursor.connection); auto p = waylandServer()->seat()->focusedPointer(); if (p) { m_serverCursor.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateServerCursor); } else { m_serverCursor.connection = QMetaObject::Connection(); reevaluteSource(); } } void CursorImage::updateDecoration() { disconnect(m_decorationConnection); auto deco = m_pointer->decoration(); AbstractClient *c = deco.isNull() ? nullptr : deco->client(); if (c) { m_decorationConnection = connect(c, &AbstractClient::moveResizeCursorChanged, this, &CursorImage::updateDecorationCursor); } else { m_decorationConnection = QMetaObject::Connection(); } updateDecorationCursor(); } void CursorImage::updateDecorationCursor() { m_decorationCursor.image = QImage(); m_decorationCursor.hotSpot = QPoint(); auto deco = m_pointer->decoration(); if (AbstractClient *c = deco.isNull() ? nullptr : deco->client()) { loadThemeCursor(c->cursor(), &m_decorationCursor); if (m_currentSource == CursorSource::Decoration) { emit changed(); } } reevaluteSource(); } void CursorImage::updateMoveResize() { m_moveResizeCursor.image = QImage(); m_moveResizeCursor.hotSpot = QPoint(); if (AbstractClient *c = workspace()->getMovingClient()) { loadThemeCursor(c->isMove() ? Qt::SizeAllCursor : Qt::SizeBDiagCursor, &m_moveResizeCursor); if (m_currentSource == CursorSource::MoveResize) { emit changed(); } } reevaluteSource(); } void CursorImage::updateServerCursor() { m_serverCursor.image = QImage(); m_serverCursor.hotSpot = QPoint(); reevaluteSource(); const bool needsEmit = m_currentSource == CursorSource::LockScreen || m_currentSource == CursorSource::PointerSurface; auto p = waylandServer()->seat()->focusedPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_serverCursor.hotSpot = c->hotspot(); m_serverCursor.image = buffer->data().copy(); m_serverCursor.image.setDevicePixelRatio(cursorSurface->scale()); if (needsEmit) { emit changed(); } } void CursorImage::loadTheme() { if (m_cursorTheme) { return; } // check whether we can create it if (waylandServer()->internalShmPool()) { m_cursorTheme = new WaylandCursorTheme(waylandServer()->internalShmPool(), this); connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, this, [this] { delete m_cursorTheme; m_cursorTheme = nullptr; } ); } } void CursorImage::setEffectsOverrideCursor(Qt::CursorShape shape) { loadThemeCursor(shape, &m_effectsCursor); if (m_currentSource == CursorSource::EffectsOverride) { emit changed(); } reevaluteSource(); } void CursorImage::removeEffectsOverrideCursor() { reevaluteSource(); } void CursorImage::setWindowSelectionCursor(const QByteArray &shape) { if (shape.isEmpty()) { loadThemeCursor(Qt::CrossCursor, &m_windowSelectionCursor); } else { loadThemeCursor(shape, &m_windowSelectionCursor); } if (m_currentSource == CursorSource::WindowSelector) { emit changed(); } reevaluteSource(); } void CursorImage::removeWindowSelectionCursor() { reevaluteSource(); } void CursorImage::updateDrag() { using namespace KWayland::Server; disconnect(m_drag.connection); m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); reevaluteSource(); if (auto p = waylandServer()->seat()->dragPointer()) { m_drag.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateDragCursor); } else { m_drag.connection = QMetaObject::Connection(); } updateDragCursor(); } void CursorImage::updateDragCursor() { m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); const bool needsEmit = m_currentSource == CursorSource::DragAndDrop; QImage additionalIcon; if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto dragIcon = ddi->icon()) { if (auto buffer = dragIcon->buffer()) { additionalIcon = buffer->data().copy(); } } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_drag.cursor.hotSpot = c->hotspot(); m_drag.cursor.image = buffer->data().copy(); if (needsEmit) { emit changed(); } // TODO: add the cursor image } void CursorImage::loadThemeCursor(CursorShape shape, Image *image) { loadThemeCursor(shape, m_cursors, image); } void CursorImage::loadThemeCursor(const QByteArray &shape, Image *image) { loadThemeCursor(shape, m_cursorsByName, image); } template void CursorImage::loadThemeCursor(const T &shape, QHash &cursors, Image *image) { loadTheme(); if (!m_cursorTheme) { return; } auto it = cursors.constFind(shape); if (it == cursors.constEnd()) { image->image = QImage(); image->hotSpot = QPoint(); wl_cursor_image *cursor = m_cursorTheme->get(shape); if (!cursor) { return; } wl_buffer *b = wl_cursor_image_get_buffer(cursor); if (!b) { return; } waylandServer()->internalClientConection()->flush(); waylandServer()->dispatch(); auto buffer = KWayland::Server::BufferInterface::get(waylandServer()->internalConnection()->getResource(KWayland::Client::Buffer::getId(b))); if (!buffer) { return; } auto scale = screens()->maxScale(); int hotSpotX = qRound(cursor->hotspot_x / scale); int hotSpotY = qRound(cursor->hotspot_y / scale); QImage img = buffer->data().copy(); img.setDevicePixelRatio(scale); it = decltype(it)(cursors.insert(shape, {img, QPoint(hotSpotX, hotSpotY)})); } image->hotSpot = it.value().hotSpot; image->image = it.value().image; } void CursorImage::reevaluteSource() { if (waylandServer()->seat()->isDragPointer()) { // TODO: touch drag? setSource(CursorSource::DragAndDrop); return; } if (waylandServer()->isScreenLocked()) { setSource(CursorSource::LockScreen); return; } if (input()->isSelectingWindow()) { setSource(CursorSource::WindowSelector); return; } if (effects && static_cast(effects)->isMouseInterception()) { setSource(CursorSource::EffectsOverride); return; } if (workspace() && workspace()->getMovingClient()) { setSource(CursorSource::MoveResize); return; } if (!m_pointer->decoration().isNull()) { setSource(CursorSource::Decoration); return; } - if (!m_pointer->window().isNull() && waylandServer()->seat()->focusedPointer()) { + if (!m_pointer->focus().isNull() && waylandServer()->seat()->focusedPointer()) { setSource(CursorSource::PointerSurface); return; } setSource(CursorSource::Fallback); } void CursorImage::setSource(CursorSource source) { if (m_currentSource == source) { return; } m_currentSource = source; emit changed(); } QImage CursorImage::image() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.image; case CursorSource::MoveResize: return m_moveResizeCursor.image; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.image; case CursorSource::Decoration: return m_decorationCursor.image; case CursorSource::DragAndDrop: return m_drag.cursor.image; case CursorSource::Fallback: return m_fallbackCursor.image; case CursorSource::WindowSelector: return m_windowSelectionCursor.image; default: Q_UNREACHABLE(); } } QPoint CursorImage::hotSpot() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.hotSpot; case CursorSource::MoveResize: return m_moveResizeCursor.hotSpot; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.hotSpot; case CursorSource::Decoration: return m_decorationCursor.hotSpot; case CursorSource::DragAndDrop: return m_drag.cursor.hotSpot; case CursorSource::Fallback: return m_fallbackCursor.hotSpot; case CursorSource::WindowSelector: return m_windowSelectionCursor.hotSpot; default: Q_UNREACHABLE(); } } } diff --git a/pointer_input.h b/pointer_input.h index f94269de2..8bdc0d99f 100644 --- a/pointer_input.h +++ b/pointer_input.h @@ -1,250 +1,258 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_POINTER_INPUT_H #define KWIN_POINTER_INPUT_H #include "input.h" #include #include #include #include class QWindow; namespace KWayland { namespace Server { class SurfaceInterface; } } namespace KWin { class CursorImage; class InputRedirection; class Toplevel; class WaylandCursorTheme; class CursorShape; namespace Decoration { class DecoratedClientImpl; } namespace LibInput { class Device; } class KWIN_EXPORT PointerInputRedirection : public InputDeviceHandler { Q_OBJECT public: explicit PointerInputRedirection(InputRedirection *parent); virtual ~PointerInputRedirection(); void init(); - void update(); void updateAfterScreenChange(); bool supportsWarping() const; void warp(const QPointF &pos); QPointF pos() const { return m_pos; } Qt::MouseButtons buttons() const { return m_qtButtons; } + bool areButtonsPressed() const; QImage cursorImage() const; QPoint cursorHotSpot() const; void markCursorAsRendered(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); void setWindowSelectionCursor(const QByteArray &shape); void removeWindowSelectionCursor(); void updatePointerConstraints(); void setEnableConstraints(bool set); bool isConstrained() const { return m_confined || m_locked; } + bool focusUpdatesBlocked() override; + /** * @internal */ void processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device = nullptr); /** * @internal **/ void processMotion(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec, LibInput::Device *device); /** * @internal */ void processButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time, LibInput::Device *device = nullptr); /** * @internal */ void processAxis(InputRedirection::PointerAxis axis, qreal delta, uint32_t time, LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureEnd(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processSwipeGestureCancelled(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureEnd(quint32 time, KWin::LibInput::Device *device = nullptr); /** * @internal */ void processPinchGestureCancelled(quint32 time, KWin::LibInput::Device *device = nullptr); private: + void cleanupInternalWindow(QWindow *old, QWindow *now) override; + void cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) override; + + void focusUpdate(Toplevel *focusOld, Toplevel *focusNow) override; + + QPointF position() const override; + void updateOnStartMoveResize(); void updateToReset(); void updatePosition(const QPointF &pos); void updateButton(uint32_t button, InputRedirection::PointerButtonState state); void warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *surface); QPointF applyPointerConfinement(const QPointF &pos) const; void disconnectConfinedPointerRegionConnection(); void disconnectLockedPointerAboutToBeUnboundConnection(); void disconnectPointerConstraintsConnection(); void breakPointerConstraints(KWayland::Server::SurfaceInterface *surface); - bool areButtonsPressed() const; CursorImage *m_cursor; - bool m_inited = false; bool m_supportsWarping; QPointF m_pos; QHash m_buttons; Qt::MouseButtons m_qtButtons; - QMetaObject::Connection m_windowGeometryConnection; + QMetaObject::Connection m_focusGeometryConnection; QMetaObject::Connection m_internalWindowConnection; QMetaObject::Connection m_constraintsConnection; QMetaObject::Connection m_constraintsActivatedConnection; QMetaObject::Connection m_confinedPointerRegionConnection; QMetaObject::Connection m_lockedPointerAboutToBeUnboundConnection; QMetaObject::Connection m_decorationGeometryConnection; bool m_confined = false; bool m_locked = false; bool m_enableConstraints = true; }; class CursorImage : public QObject { Q_OBJECT public: explicit CursorImage(PointerInputRedirection *parent = nullptr); virtual ~CursorImage(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); void setWindowSelectionCursor(const QByteArray &shape); void removeWindowSelectionCursor(); QImage image() const; QPoint hotSpot() const; void markAsRendered(); Q_SIGNALS: void changed(); private: void reevaluteSource(); void update(); void updateServerCursor(); void updateDecoration(); void updateDecorationCursor(); void updateMoveResize(); void updateDrag(); void updateDragCursor(); void loadTheme(); struct Image { QImage image; QPoint hotSpot; }; void loadThemeCursor(CursorShape shape, Image *image); void loadThemeCursor(const QByteArray &shape, Image *image); template void loadThemeCursor(const T &shape, QHash &cursors, Image *image); enum class CursorSource { LockScreen, EffectsOverride, MoveResize, PointerSurface, Decoration, DragAndDrop, Fallback, WindowSelector }; void setSource(CursorSource source); PointerInputRedirection *m_pointer; CursorSource m_currentSource = CursorSource::Fallback; WaylandCursorTheme *m_cursorTheme = nullptr; struct { QMetaObject::Connection connection; QImage image; QPoint hotSpot; } m_serverCursor; Image m_effectsCursor; Image m_decorationCursor; QMetaObject::Connection m_decorationConnection; Image m_fallbackCursor; Image m_moveResizeCursor; Image m_windowSelectionCursor; QHash m_cursors; QHash m_cursorsByName; QElapsedTimer m_surfaceRenderedTimer; struct { Image cursor; QMetaObject::Connection connection; } m_drag; }; } #endif diff --git a/touch_input.cpp b/touch_input.cpp index 5d14abdc7..4cf685cfb 100644 --- a/touch_input.cpp +++ b/touch_input.cpp @@ -1,203 +1,226 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "touch_input.h" #include "abstract_client.h" #include "input.h" +#include "pointer_input.h" #include "input_event_spy.h" #include "toplevel.h" #include "wayland_server.h" #include "workspace.h" #include "decorations/decoratedclient.h" // KDecoration #include // KWayland #include // screenlocker #include // Qt #include #include namespace KWin { TouchInputRedirection::TouchInputRedirection(InputRedirection *parent) : InputDeviceHandler(parent) { } TouchInputRedirection::~TouchInputRedirection() = default; void TouchInputRedirection::init() { - Q_ASSERT(!m_inited); - m_inited = true; + Q_ASSERT(!inited()); + setInited(true); + InputDeviceHandler::init(); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { cancel(); // position doesn't matter update(); } ); } - connect(workspace(), &QObject::destroyed, this, [this] { m_inited = false; }); - connect(waylandServer(), &QObject::destroyed, this, [this] { m_inited = false; }); + connect(workspace(), &QObject::destroyed, this, [this] { setInited(false); }); + connect(waylandServer(), &QObject::destroyed, this, [this] { setInited(false); }); } -void TouchInputRedirection::update(const QPointF &pos) +bool TouchInputRedirection::focusUpdatesBlocked() { - if (!m_inited) { - return; + if (!inited()) { + return true; } if (m_windowUpdatedInCycle) { - return; + return true; } m_windowUpdatedInCycle = true; - // TODO: handle pointer grab aka popups - Toplevel *t = input()->findToplevel(pos.toPoint()); - auto oldWindow = window(); - updateInternalWindow(pos); - if (!internalWindow()) { - updateDecoration(t, pos); - } else { - // TODO: send hover leave to decoration - if (decoration()) { - decoration()->client()->leaveEvent(); - } - clearDecoration(); + if (m_touches > 0) { + // first touch defines focus + return true; } - if (decoration() || internalWindow()) { - t = nullptr; - } else if (!decoration()) { - m_decorationId = -1; - } else if (!internalWindow()) { - m_internalId = -1; + return false; +} + +void TouchInputRedirection::focusUpdate(Toplevel *focusOld, Toplevel *focusNow) +{ + // TODO: handle pointer grab aka popups + + if (AbstractClient *ac = qobject_cast(focusOld)) { + ac->leaveEvent(); } - if (!oldWindow.isNull() && t == oldWindow.data()) { - return; + disconnect(m_focusGeometryConnection); + m_focusGeometryConnection = QMetaObject::Connection(); + + if (AbstractClient *ac = qobject_cast(focusNow)) { + ac->enterEvent(m_lastPosition.toPoint()); + workspace()->updateFocusMousePosition(m_lastPosition.toPoint()); } + auto seat = waylandServer()->seat(); - // disconnect old surface - if (oldWindow) { - disconnect(m_windowGeometryConnection); - m_windowGeometryConnection = QMetaObject::Connection(); - } - if (t && t->surface()) { - // FIXME: add input transformation API to KWayland::Server::SeatInterface for touch input - seat->setFocusedTouchSurface(t->surface(), -1 * t->inputTransformation().map(t->pos()) + t->pos()); - m_windowGeometryConnection = connect(t, &Toplevel::geometryChanged, this, - [this] { - if (window().isNull()) { - return; - } - auto seat = waylandServer()->seat(); - if (window().data()->surface() != seat->focusedTouchSurface()) { - return; - } - auto t = window().data(); - seat->setFocusedTouchSurfacePosition(-1 * t->inputTransformation().map(t->pos()) + t->pos()); - } - ); - } else { + if (!focusNow || !focusNow->surface() || decoration()) { + // no new surface or internal window or on decoration -> cleanup seat->setFocusedTouchSurface(nullptr); - t = nullptr; - } - if (!t) { - setWindow(); return; } - setWindow(t); + + // TODO: invalidate pointer focus? + + // FIXME: add input transformation API to KWayland::Server::SeatInterface for touch input + seat->setFocusedTouchSurface(focusNow->surface(), -1 * focusNow->inputTransformation().map(focusNow->pos()) + focusNow->pos()); + m_focusGeometryConnection = connect(focusNow, &Toplevel::geometryChanged, this, + [this] { + if (focus().isNull()) { + return; + } + auto seat = waylandServer()->seat(); + if (focus().data()->surface() != seat->focusedTouchSurface()) { + return; + } + seat->setFocusedTouchSurfacePosition(-1 * focus()->inputTransformation().map(focus()->pos()) + focus()->pos()); + } + ); +} + +void TouchInputRedirection::cleanupInternalWindow(QWindow *old, QWindow *now) +{ + Q_UNUSED(old); + Q_UNUSED(now); + + // nothing to do +} + +void TouchInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) +{ + Q_UNUSED(now); + + if (old) { + // send leave event to old decoration + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(old->decoration(), &event); + } } void TouchInputRedirection::insertId(quint32 internalId, qint32 kwaylandId) { m_idMapper.insert(internalId, kwaylandId); } qint32 TouchInputRedirection::mappedId(quint32 internalId) { auto it = m_idMapper.constFind(internalId); if (it != m_idMapper.constEnd()) { return it.value(); } return -1; } void TouchInputRedirection::removeId(quint32 internalId) { m_idMapper.remove(internalId); } void TouchInputRedirection::processDown(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + m_lastPosition = pos; m_windowUpdatedInCycle = false; + if (m_touches == 0) { + update(); + } + m_touches++; input()->processSpies(std::bind(&InputEventSpy::touchDown, std::placeholders::_1, id, pos, time)); input()->processFilters(std::bind(&InputEventFilter::touchDown, std::placeholders::_1, id, pos, time)); m_windowUpdatedInCycle = false; } void TouchInputRedirection::processUp(qint32 id, quint32 time, LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } m_windowUpdatedInCycle = false; input()->processSpies(std::bind(&InputEventSpy::touchUp, std::placeholders::_1, id, time)); input()->processFilters(std::bind(&InputEventFilter::touchUp, std::placeholders::_1, id, time)); m_windowUpdatedInCycle = false; + m_touches--; + if (m_touches == 0) { + update(); + } } void TouchInputRedirection::processMotion(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device) { Q_UNUSED(device) - if (!m_inited) { + if (!inited()) { return; } + m_lastPosition = pos; m_windowUpdatedInCycle = false; input()->processSpies(std::bind(&InputEventSpy::touchMotion, std::placeholders::_1, id, pos, time)); input()->processFilters(std::bind(&InputEventFilter::touchMotion, std::placeholders::_1, id, pos, time)); m_windowUpdatedInCycle = false; } void TouchInputRedirection::cancel() { - if (!m_inited) { + if (!inited()) { return; } waylandServer()->seat()->cancelTouchSequence(); m_idMapper.clear(); } void TouchInputRedirection::frame() { - if (!m_inited) { + if (!inited()) { return; } waylandServer()->seat()->touchFrame(); } } diff --git a/touch_input.h b/touch_input.h index 900c0056a..53a03166b 100644 --- a/touch_input.h +++ b/touch_input.h @@ -1,92 +1,105 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin +Copyright (C) 2018 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_TOUCH_INPUT_H #define KWIN_TOUCH_INPUT_H #include "input.h" #include #include #include #include namespace KWin { class InputRedirection; class Toplevel; namespace Decoration { class DecoratedClientImpl; } namespace LibInput { class Device; } class TouchInputRedirection : public InputDeviceHandler { Q_OBJECT public: explicit TouchInputRedirection(InputRedirection *parent); virtual ~TouchInputRedirection(); - void update(const QPointF &pos = QPointF()); + bool focusUpdatesBlocked() override; void init(); void processDown(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device = nullptr); void processUp(qint32 id, quint32 time, LibInput::Device *device = nullptr); void processMotion(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device = nullptr); void cancel(); void frame(); void insertId(quint32 internalId, qint32 kwaylandId); void removeId(quint32 internalId); qint32 mappedId(quint32 internalId); void setDecorationPressId(qint32 id) { m_decorationId = id; } qint32 decorationPressId() const { return m_decorationId; } void setInternalPressId(qint32 id) { m_internalId = id; } qint32 internalPressId() const { return m_internalId; } + QPointF position() const override { + return m_lastPosition; + } + private: + void cleanupInternalWindow(QWindow *old, QWindow *now) override; + void cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) override; + + void focusUpdate(Toplevel *focusOld, Toplevel *focusNow) override; + bool m_inited = false; qint32 m_decorationId = -1; qint32 m_internalId = -1; /** * external/kwayland **/ QHash m_idMapper; - QMetaObject::Connection m_windowGeometryConnection; + QMetaObject::Connection m_focusGeometryConnection; bool m_windowUpdatedInCycle = false; + QPointF m_lastPosition; + + int m_touches = 0; }; } #endif