File indexing completed on 2024-05-12 05:30:41

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
0006     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "kwin_wayland_test.h"
0012 
0013 #include "atoms.h"
0014 #include "cursor.h"
0015 #include "effect/effectloader.h"
0016 #include "main.h"
0017 #include "pointer_input.h"
0018 #include "screenedge.h"
0019 #include "wayland_server.h"
0020 #include "window.h"
0021 #include "workspace.h"
0022 
0023 #include <KConfigGroup>
0024 #include <KWayland/Client/surface.h>
0025 
0026 #include <QAbstractEventDispatcher>
0027 #include <QAction>
0028 #include <QSocketNotifier>
0029 
0030 #include <xcb/xcb_icccm.h>
0031 
0032 Q_DECLARE_METATYPE(KWin::ElectricBorder)
0033 
0034 namespace KWin
0035 {
0036 
0037 static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen-edges-0");
0038 
0039 class TestObject : public QObject
0040 {
0041     Q_OBJECT
0042 
0043 public Q_SLOTS:
0044     bool callback(ElectricBorder border)
0045     {
0046         Q_EMIT gotCallback(border);
0047         return true;
0048     }
0049 
0050 Q_SIGNALS:
0051     void gotCallback(KWin::ElectricBorder);
0052 };
0053 
0054 class ScreenEdgesTest : public QObject
0055 {
0056     Q_OBJECT
0057 
0058 private Q_SLOTS:
0059     void initTestCase();
0060     void init();
0061     void cleanup();
0062     void testTouchCallback_data();
0063     void testTouchCallback();
0064     void testPushBack_data();
0065     void testPushBack();
0066     void testObjectEdge_data();
0067     void testObjectEdge();
0068     void testKdeNetWmScreenEdgeShow();
0069 };
0070 
0071 void ScreenEdgesTest::initTestCase()
0072 {
0073     qRegisterMetaType<KWin::Window *>();
0074     qRegisterMetaType<KWin::ElectricBorder>("ElectricBorder");
0075 
0076     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
0077     QVERIFY(waylandServer()->init(s_socketName));
0078     Test::setOutputConfig({QRect(0, 0, 1280, 1024)});
0079 
0080     // Disable effects, in particular present windows, which reserves a screen edge.
0081     auto config = kwinApp()->config();
0082     KConfigGroup plugins(config, QStringLiteral("Plugins"));
0083     const auto builtinNames = EffectLoader().listOfKnownEffects();
0084     for (const QString &name : builtinNames) {
0085         plugins.writeEntry(name + QStringLiteral("Enabled"), false);
0086     }
0087 
0088     config->sync();
0089     kwinApp()->setConfig(config);
0090 
0091     kwinApp()->start();
0092     QVERIFY(applicationStartedSpy.wait());
0093 }
0094 
0095 void ScreenEdgesTest::init()
0096 {
0097     workspace()->screenEdges()->recreateEdges();
0098     Workspace::self()->setActiveOutput(QPoint(640, 512));
0099     KWin::input()->pointer()->warp(QPoint(640, 512));
0100 
0101     QVERIFY(Test::setupWaylandConnection());
0102 }
0103 
0104 void ScreenEdgesTest::cleanup()
0105 {
0106     Test::destroyWaylandConnection();
0107 }
0108 
0109 void ScreenEdgesTest::testTouchCallback_data()
0110 {
0111     QTest::addColumn<KWin::ElectricBorder>("border");
0112     QTest::addColumn<QPointF>("startPos");
0113     QTest::addColumn<QPointF>("delta");
0114 
0115     QTest::newRow("left") << ElectricLeft << QPointF(0, 50) << QPointF(256, 20);
0116     QTest::newRow("top") << ElectricTop << QPointF(50, 0) << QPointF(20, 250);
0117     QTest::newRow("right") << ElectricRight << QPointF(1279, 50) << QPointF(-256, 0);
0118     QTest::newRow("bottom") << ElectricBottom << QPointF(50, 1023) << QPointF(0, -205);
0119 }
0120 
0121 void ScreenEdgesTest::testTouchCallback()
0122 {
0123     // This test verifies that touch screen edges trigger associated callbacks.
0124 
0125     auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
0126     auto group = config->group(QStringLiteral("TouchEdges"));
0127     group.writeEntry("Top", "none");
0128     group.writeEntry("Left", "none");
0129     group.writeEntry("Bottom", "none");
0130     group.writeEntry("Right", "none");
0131     config->sync();
0132 
0133     auto s = workspace()->screenEdges();
0134     s->setConfig(config);
0135     s->reconfigure();
0136 
0137     // none of our actions should be reserved
0138     const auto &edges = s->edges();
0139     QCOMPARE(edges.size(), 8);
0140     for (auto &edge : edges) {
0141         QCOMPARE(edge->isReserved(), false);
0142         QCOMPARE(edge->activatesForPointer(), false);
0143         QCOMPARE(edge->activatesForTouchGesture(), false);
0144     }
0145 
0146     // let's reserve an action
0147     QAction action;
0148     QSignalSpy actionTriggeredSpy(&action, &QAction::triggered);
0149 
0150     // reserve on edge
0151     QFETCH(KWin::ElectricBorder, border);
0152     s->reserveTouch(border, &action);
0153     for (auto &edge : edges) {
0154         QCOMPARE(edge->isReserved(), edge->border() == border);
0155         QCOMPARE(edge->activatesForPointer(), false);
0156         QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
0157     }
0158 
0159     quint32 timestamp = 0;
0160 
0161     // press the finger
0162     QFETCH(QPointF, startPos);
0163     Test::touchDown(1, startPos, timestamp++);
0164     QVERIFY(actionTriggeredSpy.isEmpty());
0165 
0166     // move the finger
0167     QFETCH(QPointF, delta);
0168     Test::touchMotion(1, startPos + delta, timestamp++);
0169     QVERIFY(actionTriggeredSpy.isEmpty());
0170 
0171     // release the finger
0172     Test::touchUp(1, timestamp++);
0173     QVERIFY(actionTriggeredSpy.wait());
0174     QCOMPARE(actionTriggeredSpy.count(), 1);
0175 
0176     // unreserve again
0177     s->unreserveTouch(border, &action);
0178     for (auto &edge : edges) {
0179         QCOMPARE(edge->isReserved(), false);
0180         QCOMPARE(edge->activatesForPointer(), false);
0181         QCOMPARE(edge->activatesForTouchGesture(), false);
0182     }
0183 
0184     // reserve another action
0185     std::unique_ptr<QAction> action2(new QAction);
0186     s->reserveTouch(border, action2.get());
0187     for (auto &edge : edges) {
0188         QCOMPARE(edge->isReserved(), edge->border() == border);
0189         QCOMPARE(edge->activatesForPointer(), false);
0190         QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
0191     }
0192 
0193     // and unreserve by destroying
0194     action2.reset();
0195     for (auto &edge : edges) {
0196         QCOMPARE(edge->isReserved(), false);
0197         QCOMPARE(edge->activatesForPointer(), false);
0198         QCOMPARE(edge->activatesForTouchGesture(), false);
0199     }
0200 }
0201 
0202 void ScreenEdgesTest::testPushBack_data()
0203 {
0204     QTest::addColumn<KWin::ElectricBorder>("border");
0205     QTest::addColumn<int>("pushback");
0206     QTest::addColumn<QPointF>("trigger");
0207     QTest::addColumn<QPointF>("expected");
0208 
0209     QTest::newRow("top-left-3") << ElectricTopLeft << 3 << QPointF(0, 0) << QPointF(3, 3);
0210     QTest::newRow("top-5") << ElectricTop << 5 << QPointF(50, 0) << QPointF(50, 5);
0211     QTest::newRow("top-right-2") << ElectricTopRight << 2 << QPointF(1279, 0) << QPointF(1277, 2);
0212     QTest::newRow("right-10") << ElectricRight << 10 << QPointF(1279, 50) << QPointF(1269, 50);
0213     QTest::newRow("bottom-right-5") << ElectricBottomRight << 5 << QPointF(1279, 1023) << QPointF(1274, 1018);
0214     QTest::newRow("bottom-10") << ElectricBottom << 10 << QPointF(50, 1023) << QPointF(50, 1013);
0215     QTest::newRow("bottom-left-3") << ElectricBottomLeft << 3 << QPointF(0, 1023) << QPointF(3, 1020);
0216     QTest::newRow("left-10") << ElectricLeft << 10 << QPointF(0, 50) << QPointF(10, 50);
0217     QTest::newRow("invalid") << ElectricLeft << 10 << QPointF(50, 0) << QPointF(50, 0);
0218 }
0219 
0220 void ScreenEdgesTest::testPushBack()
0221 {
0222     // This test verifies that the pointer will be pushed back if it approached a screen edge.
0223 
0224     QFETCH(int, pushback);
0225     auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
0226     config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderPushbackPixels", pushback);
0227     config->sync();
0228 
0229     auto s = workspace()->screenEdges();
0230     s->setConfig(config);
0231     s->reconfigure();
0232 
0233     TestObject callback;
0234     QSignalSpy spy(&callback, &TestObject::gotCallback);
0235 
0236     QFETCH(ElectricBorder, border);
0237     s->reserve(border, &callback, "callback");
0238 
0239     QFETCH(QPointF, trigger);
0240     Test::pointerMotion(trigger, 0);
0241     QVERIFY(spy.isEmpty());
0242     QTEST(Cursors::self()->mouse()->pos(), "expected");
0243 }
0244 
0245 void ScreenEdgesTest::testObjectEdge_data()
0246 {
0247     QTest::addColumn<ElectricBorder>("border");
0248     QTest::addColumn<QPointF>("triggerPoint");
0249     QTest::addColumn<QPointF>("delta");
0250 
0251     QTest::newRow("top") << ElectricTop << QPointF(640, 0) << QPointF(0, 50);
0252     QTest::newRow("right") << ElectricRight << QPointF(1279, 512) << QPointF(-50, 0);
0253     QTest::newRow("bottom") << ElectricBottom << QPointF(640, 1023) << QPointF(0, -50);
0254     QTest::newRow("left") << ElectricLeft << QPointF(0, 512) << QPointF(50, 0);
0255 }
0256 
0257 void ScreenEdgesTest::testObjectEdge()
0258 {
0259     // This test verifies that a screen edge reserved by a script or any QObject is activated.
0260 
0261     TestObject callback;
0262     QSignalSpy spy(&callback, &TestObject::gotCallback);
0263 
0264     // Reserve a screen edge border.
0265     QFETCH(ElectricBorder, border);
0266     workspace()->screenEdges()->reserve(border, &callback, "callback");
0267 
0268     QFETCH(QPointF, triggerPoint);
0269     QFETCH(QPointF, delta);
0270 
0271     // doesn't trigger as the edge was not triggered yet
0272     qint64 timestamp = 0;
0273     Test::pointerMotion(triggerPoint + delta, timestamp);
0274     QVERIFY(spy.isEmpty());
0275 
0276     // test doesn't trigger due to too much offset
0277     timestamp += 160;
0278     Test::pointerMotion(triggerPoint, timestamp);
0279     QVERIFY(spy.isEmpty());
0280 
0281     // doesn't activate as we are waiting too short
0282     timestamp += 50;
0283     Test::pointerMotion(triggerPoint, timestamp);
0284     QVERIFY(spy.isEmpty());
0285 
0286     // and this one triggers
0287     timestamp += 110;
0288     Test::pointerMotion(triggerPoint, timestamp);
0289     QVERIFY(!spy.isEmpty());
0290 
0291     // now let's try to trigger again
0292     timestamp += 351;
0293     Test::pointerMotion(triggerPoint, timestamp);
0294     QCOMPARE(spy.count(), 1);
0295 
0296     // it's still under the reactivation
0297     timestamp += 50;
0298     Test::pointerMotion(triggerPoint, timestamp);
0299     QCOMPARE(spy.count(), 1);
0300 
0301     // now it should trigger again
0302     timestamp += 250;
0303     Test::pointerMotion(triggerPoint, timestamp);
0304     QCOMPARE(spy.count(), 2);
0305 }
0306 
0307 static void enableAutoHide(xcb_connection_t *connection, xcb_window_t windowId, ElectricBorder border)
0308 {
0309     if (border == ElectricNone) {
0310         xcb_delete_property(connection, windowId, atoms->kde_screen_edge_show);
0311     } else {
0312         uint32_t value = 0;
0313 
0314         switch (border) {
0315         case ElectricTop:
0316             value = 0;
0317             break;
0318         case ElectricRight:
0319             value = 1;
0320             break;
0321         case ElectricBottom:
0322             value = 2;
0323             break;
0324         case ElectricLeft:
0325             value = 3;
0326             break;
0327         default:
0328             Q_UNREACHABLE();
0329         }
0330 
0331         xcb_change_property(connection, XCB_PROP_MODE_REPLACE, windowId, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &value);
0332     }
0333 }
0334 
0335 class ScreenEdgePropertyMonitor : public QObject
0336 {
0337     Q_OBJECT
0338 public:
0339     ScreenEdgePropertyMonitor(xcb_connection_t *c, xcb_window_t window)
0340         : QObject()
0341         , m_connection(c)
0342         , m_window(window)
0343         , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this))
0344     {
0345         connect(m_notifier, &QSocketNotifier::activated, this, &ScreenEdgePropertyMonitor::processXcbEvents);
0346         connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &ScreenEdgePropertyMonitor::processXcbEvents);
0347         connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &ScreenEdgePropertyMonitor::processXcbEvents);
0348     }
0349 
0350 Q_SIGNALS:
0351     void withdrawn();
0352 
0353 private:
0354     void processXcbEvents()
0355     {
0356         while (auto event = xcb_poll_for_event(m_connection)) {
0357             const uint8_t eventType = event->response_type & ~0x80;
0358             switch (eventType) {
0359             case XCB_PROPERTY_NOTIFY: {
0360                 auto propertyNotifyEvent = reinterpret_cast<xcb_property_notify_event_t *>(event);
0361                 if (propertyNotifyEvent->window == m_window && propertyNotifyEvent->atom == atoms->kde_screen_edge_show && propertyNotifyEvent->state == XCB_PROPERTY_DELETE) {
0362                     Q_EMIT withdrawn();
0363                 }
0364                 break;
0365             }
0366             }
0367             free(event);
0368         }
0369     }
0370 
0371     xcb_connection_t *m_connection;
0372     xcb_window_t m_window;
0373     QSocketNotifier *m_notifier;
0374 };
0375 
0376 void ScreenEdgesTest::testKdeNetWmScreenEdgeShow()
0377 {
0378     // This test verifies that _KDE_NET_WM_SCREEN_EDGE_SHOW is handled properly. Note that
0379     // _KDE_NET_WM_SCREEN_EDGE_SHOW has oneshot effect. It's deleted when the window is shown.
0380 
0381     auto config = kwinApp()->config();
0382     config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderDelay", 75);
0383     config->sync();
0384     workspace()->slotReconfigure();
0385 
0386     Test::XcbConnectionPtr c = Test::createX11Connection();
0387     QVERIFY(!xcb_connection_has_error(c.get()));
0388 
0389     // Create a test window at the bottom of the screen.
0390     const QRect windowGeometry(0, 1024 - 30, 1280, 30);
0391     const uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
0392     xcb_window_t windowId = xcb_generate_id(c.get());
0393     xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(),
0394                       windowGeometry.x(),
0395                       windowGeometry.y(),
0396                       windowGeometry.width(),
0397                       windowGeometry.height(),
0398                       0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
0399                       XCB_COPY_FROM_PARENT,
0400                       XCB_CW_EVENT_MASK, values);
0401     xcb_size_hints_t hints;
0402     memset(&hints, 0, sizeof(hints));
0403     xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y());
0404     xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height());
0405     xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints);
0406     xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId);
0407     xcb_map_window(c.get(), windowId);
0408     xcb_flush(c.get());
0409 
0410     QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded);
0411     QVERIFY(windowCreatedSpy.wait());
0412     Window *window = windowCreatedSpy.first().first().value<Window *>();
0413     QVERIFY(window);
0414 
0415     ScreenEdgePropertyMonitor screenEdgeMonitor(c.get(), windowId);
0416     QSignalSpy withdrawnSpy(&screenEdgeMonitor, &ScreenEdgePropertyMonitor::withdrawn);
0417     QSignalSpy windowShownSpy(window, &Window::windowShown);
0418     QSignalSpy windowHiddenSpy(window, &Window::windowHidden);
0419     quint32 timestamp = 0;
0420 
0421     // The window will be shown when the pointer approaches its reserved screen edge.
0422     {
0423         enableAutoHide(c.get(), windowId, ElectricBottom);
0424         xcb_flush(c.get());
0425         QVERIFY(windowHiddenSpy.wait());
0426         QVERIFY(!window->isShown());
0427 
0428         Test::pointerMotion(QPointF(640, 1023), timestamp);
0429         timestamp += 160;
0430         Test::pointerMotion(QPointF(640, 1023), timestamp);
0431         QVERIFY(withdrawnSpy.wait());
0432         QVERIFY(window->isShown());
0433         timestamp += 160;
0434         Test::pointerMotion(QPointF(640, 512), timestamp);
0435         QVERIFY(window->isShown());
0436     }
0437 
0438     // The window will be shown when swiping on the touch screen.
0439     {
0440         enableAutoHide(c.get(), windowId, ElectricBottom);
0441         xcb_flush(c.get());
0442         QVERIFY(windowHiddenSpy.wait());
0443         QVERIFY(!window->isShown());
0444 
0445         Test::touchDown(0, QPointF(640, 1023), timestamp++);
0446         Test::touchMotion(0, QPointF(640, 512), timestamp++);
0447         Test::touchUp(0, timestamp++);
0448         QVERIFY(withdrawnSpy.wait());
0449         QVERIFY(window->isShown());
0450     }
0451 
0452     // The screen edge reservation won't be affected when recreating screen edges (can happen when the screen layout changes).
0453     {
0454         enableAutoHide(c.get(), windowId, ElectricBottom);
0455         xcb_flush(c.get());
0456         QVERIFY(windowHiddenSpy.wait());
0457         QVERIFY(!window->isShown());
0458 
0459         workspace()->screenEdges()->recreateEdges();
0460         QVERIFY(!withdrawnSpy.wait(50));
0461         QVERIFY(!window->isShown());
0462 
0463         enableAutoHide(c.get(), windowId, ElectricNone);
0464         xcb_flush(c.get());
0465         QVERIFY(windowShownSpy.wait());
0466         QVERIFY(window->isShown());
0467     }
0468 
0469     // The window will be shown and hidden in response to changing _KDE_NET_WM_SCREEN_EDGE_SHOW.
0470     {
0471         enableAutoHide(c.get(), windowId, ElectricBottom);
0472         xcb_flush(c.get());
0473         QVERIFY(windowHiddenSpy.wait());
0474         QVERIFY(!window->isShown());
0475 
0476         enableAutoHide(c.get(), windowId, ElectricNone);
0477         xcb_flush(c.get());
0478         QVERIFY(windowShownSpy.wait());
0479         QVERIFY(window->isShown());
0480     }
0481 
0482     // The approaching state will be reset if the window is shown manually.
0483     {
0484         QSignalSpy approachingSpy(workspace()->screenEdges(), &ScreenEdges::approaching);
0485         enableAutoHide(c.get(), windowId, ElectricBottom);
0486         xcb_flush(c.get());
0487         QVERIFY(windowHiddenSpy.wait());
0488         QVERIFY(!window->isShown());
0489 
0490         Test::pointerMotion(QPointF(640, 1020), timestamp++);
0491         QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
0492         Test::pointerMotion(QPointF(640, 1021), timestamp++);
0493         QVERIFY(approachingSpy.last().at(1).toReal() != 0.0);
0494 
0495         enableAutoHide(c.get(), windowId, ElectricNone);
0496         xcb_flush(c.get());
0497         QVERIFY(windowShownSpy.wait());
0498         QVERIFY(window->isShown());
0499         QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
0500 
0501         Test::pointerMotion(QPointF(640, 512), timestamp++);
0502     }
0503 }
0504 
0505 } // namespace KWin
0506 
0507 WAYLANDTEST_MAIN(KWin::ScreenEdgesTest)
0508 #include "screenedges_test.moc"