KWin
Loading...
Searching...
No Matches
screenedges_test.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
6 SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10
11#include "kwin_wayland_test.h"
12
13#include "atoms.h"
14#include "cursor.h"
15#include "effect/effectloader.h"
16#include "main.h"
17#include "pointer_input.h"
18#include "screenedge.h"
19#include "wayland_server.h"
20#include "window.h"
21#include "workspace.h"
22
23#include <KConfigGroup>
24#include <KWayland/Client/surface.h>
25
26#include <QAbstractEventDispatcher>
27#include <QAction>
28#include <QSocketNotifier>
29
30#include <xcb/xcb_icccm.h>
31
33
34namespace KWin
35{
36
37static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen-edges-0");
38
39class TestObject : public QObject
40{
41 Q_OBJECT
42
43public Q_SLOTS:
45 {
46 Q_EMIT gotCallback(border);
47 return true;
48 }
49
50Q_SIGNALS:
52};
53
54class ScreenEdgesTest : public QObject
55{
56 Q_OBJECT
57
58private Q_SLOTS:
59 void initTestCase();
60 void init();
61 void cleanup();
62 void testTouchCallback_data();
63 void testTouchCallback();
64 void testPushBack_data();
65 void testPushBack();
66 void testObjectEdge_data();
67 void testObjectEdge();
68 void testKdeNetWmScreenEdgeShow();
69};
70
71void ScreenEdgesTest::initTestCase()
72{
73 qRegisterMetaType<KWin::Window *>();
74 qRegisterMetaType<KWin::ElectricBorder>("ElectricBorder");
75
76 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
77 QVERIFY(waylandServer()->init(s_socketName));
78 Test::setOutputConfig({QRect(0, 0, 1280, 1024)});
79
80 // Disable effects, in particular present windows, which reserves a screen edge.
81 auto config = kwinApp()->config();
82 KConfigGroup plugins(config, QStringLiteral("Plugins"));
83 const auto builtinNames = EffectLoader().listOfKnownEffects();
84 for (const QString &name : builtinNames) {
85 plugins.writeEntry(name + QStringLiteral("Enabled"), false);
86 }
87
88 config->sync();
89 kwinApp()->setConfig(config);
90
91 kwinApp()->start();
92 QVERIFY(applicationStartedSpy.wait());
93}
94
95void ScreenEdgesTest::init()
96{
98 Workspace::self()->setActiveOutput(QPoint(640, 512));
99 KWin::input()->pointer()->warp(QPoint(640, 512));
100
102}
103
104void ScreenEdgesTest::cleanup()
105{
107}
108
109void ScreenEdgesTest::testTouchCallback_data()
110{
111 QTest::addColumn<KWin::ElectricBorder>("border");
112 QTest::addColumn<QPointF>("startPos");
113 QTest::addColumn<QPointF>("delta");
114
115 QTest::newRow("left") << ElectricLeft << QPointF(0, 50) << QPointF(256, 20);
116 QTest::newRow("top") << ElectricTop << QPointF(50, 0) << QPointF(20, 250);
117 QTest::newRow("right") << ElectricRight << QPointF(1279, 50) << QPointF(-256, 0);
118 QTest::newRow("bottom") << ElectricBottom << QPointF(50, 1023) << QPointF(0, -205);
119}
120
121void ScreenEdgesTest::testTouchCallback()
122{
123 // This test verifies that touch screen edges trigger associated callbacks.
124
125 auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
126 auto group = config->group(QStringLiteral("TouchEdges"));
127 group.writeEntry("Top", "none");
128 group.writeEntry("Left", "none");
129 group.writeEntry("Bottom", "none");
130 group.writeEntry("Right", "none");
131 config->sync();
132
133 auto s = workspace()->screenEdges();
134 s->setConfig(config);
135 s->reconfigure();
136
137 // none of our actions should be reserved
138 const auto &edges = s->edges();
139 QCOMPARE(edges.size(), 8);
140 for (auto &edge : edges) {
141 QCOMPARE(edge->isReserved(), false);
142 QCOMPARE(edge->activatesForPointer(), false);
143 QCOMPARE(edge->activatesForTouchGesture(), false);
144 }
145
146 // let's reserve an action
147 QAction action;
148 QSignalSpy actionTriggeredSpy(&action, &QAction::triggered);
149
150 // reserve on edge
151 QFETCH(KWin::ElectricBorder, border);
152 s->reserveTouch(border, &action);
153 for (auto &edge : edges) {
154 QCOMPARE(edge->isReserved(), edge->border() == border);
155 QCOMPARE(edge->activatesForPointer(), false);
156 QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
157 }
158
159 quint32 timestamp = 0;
160
161 // press the finger
162 QFETCH(QPointF, startPos);
163 Test::touchDown(1, startPos, timestamp++);
164 QVERIFY(actionTriggeredSpy.isEmpty());
165
166 // move the finger
167 QFETCH(QPointF, delta);
168 Test::touchMotion(1, startPos + delta, timestamp++);
169 QVERIFY(actionTriggeredSpy.isEmpty());
170
171 // release the finger
172 Test::touchUp(1, timestamp++);
173 QVERIFY(actionTriggeredSpy.wait());
174 QCOMPARE(actionTriggeredSpy.count(), 1);
175
176 // unreserve again
177 s->unreserveTouch(border, &action);
178 for (auto &edge : edges) {
179 QCOMPARE(edge->isReserved(), false);
180 QCOMPARE(edge->activatesForPointer(), false);
181 QCOMPARE(edge->activatesForTouchGesture(), false);
182 }
183
184 // reserve another action
185 std::unique_ptr<QAction> action2(new QAction);
186 s->reserveTouch(border, action2.get());
187 for (auto &edge : edges) {
188 QCOMPARE(edge->isReserved(), edge->border() == border);
189 QCOMPARE(edge->activatesForPointer(), false);
190 QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border);
191 }
192
193 // and unreserve by destroying
194 action2.reset();
195 for (auto &edge : edges) {
196 QCOMPARE(edge->isReserved(), false);
197 QCOMPARE(edge->activatesForPointer(), false);
198 QCOMPARE(edge->activatesForTouchGesture(), false);
199 }
200}
201
202void ScreenEdgesTest::testPushBack_data()
203{
204 QTest::addColumn<KWin::ElectricBorder>("border");
205 QTest::addColumn<int>("pushback");
206 QTest::addColumn<QPointF>("trigger");
207 QTest::addColumn<QPointF>("expected");
208
209 QTest::newRow("top-left-3") << ElectricTopLeft << 3 << QPointF(0, 0) << QPointF(3, 3);
210 QTest::newRow("top-5") << ElectricTop << 5 << QPointF(50, 0) << QPointF(50, 5);
211 QTest::newRow("top-right-2") << ElectricTopRight << 2 << QPointF(1279, 0) << QPointF(1277, 2);
212 QTest::newRow("right-10") << ElectricRight << 10 << QPointF(1279, 50) << QPointF(1269, 50);
213 QTest::newRow("bottom-right-5") << ElectricBottomRight << 5 << QPointF(1279, 1023) << QPointF(1274, 1018);
214 QTest::newRow("bottom-10") << ElectricBottom << 10 << QPointF(50, 1023) << QPointF(50, 1013);
215 QTest::newRow("bottom-left-3") << ElectricBottomLeft << 3 << QPointF(0, 1023) << QPointF(3, 1020);
216 QTest::newRow("left-10") << ElectricLeft << 10 << QPointF(0, 50) << QPointF(10, 50);
217 QTest::newRow("invalid") << ElectricLeft << 10 << QPointF(50, 0) << QPointF(50, 0);
218}
219
220void ScreenEdgesTest::testPushBack()
221{
222 // This test verifies that the pointer will be pushed back if it approached a screen edge.
223
224 QFETCH(int, pushback);
225 auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
226 config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderPushbackPixels", pushback);
227 config->sync();
228
229 auto s = workspace()->screenEdges();
230 s->setConfig(config);
231 s->reconfigure();
232
233 TestObject callback;
234 QSignalSpy spy(&callback, &TestObject::gotCallback);
235
236 QFETCH(ElectricBorder, border);
237 s->reserve(border, &callback, "callback");
238
239 QFETCH(QPointF, trigger);
240 Test::pointerMotion(trigger, 0);
241 QVERIFY(spy.isEmpty());
242 QTEST(Cursors::self()->mouse()->pos(), "expected");
243}
244
245void ScreenEdgesTest::testObjectEdge_data()
246{
247 QTest::addColumn<ElectricBorder>("border");
248 QTest::addColumn<QPointF>("triggerPoint");
249 QTest::addColumn<QPointF>("delta");
250
251 QTest::newRow("top") << ElectricTop << QPointF(640, 0) << QPointF(0, 50);
252 QTest::newRow("right") << ElectricRight << QPointF(1279, 512) << QPointF(-50, 0);
253 QTest::newRow("bottom") << ElectricBottom << QPointF(640, 1023) << QPointF(0, -50);
254 QTest::newRow("left") << ElectricLeft << QPointF(0, 512) << QPointF(50, 0);
255}
256
257void ScreenEdgesTest::testObjectEdge()
258{
259 // This test verifies that a screen edge reserved by a script or any QObject is activated.
260
261 TestObject callback;
262 QSignalSpy spy(&callback, &TestObject::gotCallback);
263
264 // Reserve a screen edge border.
265 QFETCH(ElectricBorder, border);
266 workspace()->screenEdges()->reserve(border, &callback, "callback");
267
268 QFETCH(QPointF, triggerPoint);
269 QFETCH(QPointF, delta);
270
271 // doesn't trigger as the edge was not triggered yet
272 qint64 timestamp = 0;
273 Test::pointerMotion(triggerPoint + delta, timestamp);
274 QVERIFY(spy.isEmpty());
275
276 // test doesn't trigger due to too much offset
277 timestamp += 160;
278 Test::pointerMotion(triggerPoint, timestamp);
279 QVERIFY(spy.isEmpty());
280
281 // doesn't activate as we are waiting too short
282 timestamp += 50;
283 Test::pointerMotion(triggerPoint, timestamp);
284 QVERIFY(spy.isEmpty());
285
286 // and this one triggers
287 timestamp += 110;
288 Test::pointerMotion(triggerPoint, timestamp);
289 QVERIFY(!spy.isEmpty());
290
291 // now let's try to trigger again
292 timestamp += 351;
293 Test::pointerMotion(triggerPoint, timestamp);
294 QCOMPARE(spy.count(), 1);
295
296 // it's still under the reactivation
297 timestamp += 50;
298 Test::pointerMotion(triggerPoint, timestamp);
299 QCOMPARE(spy.count(), 1);
300
301 // now it should trigger again
302 timestamp += 250;
303 Test::pointerMotion(triggerPoint, timestamp);
304 QCOMPARE(spy.count(), 2);
305}
306
307static void enableAutoHide(xcb_connection_t *connection, xcb_window_t windowId, ElectricBorder border)
308{
309 if (border == ElectricNone) {
310 xcb_delete_property(connection, windowId, atoms->kde_screen_edge_show);
311 } else {
312 uint32_t value = 0;
313
314 switch (border) {
315 case ElectricTop:
316 value = 0;
317 break;
318 case ElectricRight:
319 value = 1;
320 break;
321 case ElectricBottom:
322 value = 2;
323 break;
324 case ElectricLeft:
325 value = 3;
326 break;
327 default:
328 Q_UNREACHABLE();
329 }
330
331 xcb_change_property(connection, XCB_PROP_MODE_REPLACE, windowId, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &value);
332 }
333}
334
335class ScreenEdgePropertyMonitor : public QObject
336{
337 Q_OBJECT
338public:
339 ScreenEdgePropertyMonitor(xcb_connection_t *c, xcb_window_t window)
340 : QObject()
341 , m_connection(c)
342 , m_window(window)
343 , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this))
344 {
345 connect(m_notifier, &QSocketNotifier::activated, this, &ScreenEdgePropertyMonitor::processXcbEvents);
346 connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &ScreenEdgePropertyMonitor::processXcbEvents);
347 connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &ScreenEdgePropertyMonitor::processXcbEvents);
348 }
349
350Q_SIGNALS:
351 void withdrawn();
352
353private:
354 void processXcbEvents()
355 {
356 while (auto event = xcb_poll_for_event(m_connection)) {
357 const uint8_t eventType = event->response_type & ~0x80;
358 switch (eventType) {
359 case XCB_PROPERTY_NOTIFY: {
360 auto propertyNotifyEvent = reinterpret_cast<xcb_property_notify_event_t *>(event);
361 if (propertyNotifyEvent->window == m_window && propertyNotifyEvent->atom == atoms->kde_screen_edge_show && propertyNotifyEvent->state == XCB_PROPERTY_DELETE) {
362 Q_EMIT withdrawn();
363 }
364 break;
365 }
366 }
367 free(event);
368 }
369 }
370
371 xcb_connection_t *m_connection;
372 xcb_window_t m_window;
373 QSocketNotifier *m_notifier;
374};
375
376void ScreenEdgesTest::testKdeNetWmScreenEdgeShow()
377{
378 // This test verifies that _KDE_NET_WM_SCREEN_EDGE_SHOW is handled properly. Note that
379 // _KDE_NET_WM_SCREEN_EDGE_SHOW has oneshot effect. It's deleted when the window is shown.
380
381 auto config = kwinApp()->config();
382 config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderDelay", 75);
383 config->sync();
385
387 QVERIFY(!xcb_connection_has_error(c.get()));
388
389 // Create a test window at the bottom of the screen.
390 const QRect windowGeometry(0, 1024 - 30, 1280, 30);
391 const uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
392 xcb_window_t windowId = xcb_generate_id(c.get());
393 xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(),
394 windowGeometry.x(),
395 windowGeometry.y(),
396 windowGeometry.width(),
397 windowGeometry.height(),
398 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
399 XCB_COPY_FROM_PARENT,
400 XCB_CW_EVENT_MASK, values);
401 xcb_size_hints_t hints;
402 memset(&hints, 0, sizeof(hints));
403 xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y());
404 xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height());
405 xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints);
406 xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId);
407 xcb_map_window(c.get(), windowId);
408 xcb_flush(c.get());
409
410 QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded);
411 QVERIFY(windowCreatedSpy.wait());
412 Window *window = windowCreatedSpy.first().first().value<Window *>();
413 QVERIFY(window);
414
415 ScreenEdgePropertyMonitor screenEdgeMonitor(c.get(), windowId);
416 QSignalSpy withdrawnSpy(&screenEdgeMonitor, &ScreenEdgePropertyMonitor::withdrawn);
417 QSignalSpy windowShownSpy(window, &Window::windowShown);
418 QSignalSpy windowHiddenSpy(window, &Window::windowHidden);
419 quint32 timestamp = 0;
420
421 // The window will be shown when the pointer approaches its reserved screen edge.
422 {
423 enableAutoHide(c.get(), windowId, ElectricBottom);
424 xcb_flush(c.get());
425 QVERIFY(windowHiddenSpy.wait());
426 QVERIFY(!window->isShown());
427
428 Test::pointerMotion(QPointF(640, 1023), timestamp);
429 timestamp += 160;
430 Test::pointerMotion(QPointF(640, 1023), timestamp);
431 QVERIFY(withdrawnSpy.wait());
432 QVERIFY(window->isShown());
433 timestamp += 160;
434 Test::pointerMotion(QPointF(640, 512), timestamp);
435 QVERIFY(window->isShown());
436 }
437
438 // The window will be shown when swiping on the touch screen.
439 {
440 enableAutoHide(c.get(), windowId, ElectricBottom);
441 xcb_flush(c.get());
442 QVERIFY(windowHiddenSpy.wait());
443 QVERIFY(!window->isShown());
444
445 Test::touchDown(0, QPointF(640, 1023), timestamp++);
446 Test::touchMotion(0, QPointF(640, 512), timestamp++);
447 Test::touchUp(0, timestamp++);
448 QVERIFY(withdrawnSpy.wait());
449 QVERIFY(window->isShown());
450 }
451
452 // The screen edge reservation won't be affected when recreating screen edges (can happen when the screen layout changes).
453 {
454 enableAutoHide(c.get(), windowId, ElectricBottom);
455 xcb_flush(c.get());
456 QVERIFY(windowHiddenSpy.wait());
457 QVERIFY(!window->isShown());
458
460 QVERIFY(!withdrawnSpy.wait(50));
461 QVERIFY(!window->isShown());
462
463 enableAutoHide(c.get(), windowId, ElectricNone);
464 xcb_flush(c.get());
465 QVERIFY(windowShownSpy.wait());
466 QVERIFY(window->isShown());
467 }
468
469 // The window will be shown and hidden in response to changing _KDE_NET_WM_SCREEN_EDGE_SHOW.
470 {
471 enableAutoHide(c.get(), windowId, ElectricBottom);
472 xcb_flush(c.get());
473 QVERIFY(windowHiddenSpy.wait());
474 QVERIFY(!window->isShown());
475
476 enableAutoHide(c.get(), windowId, ElectricNone);
477 xcb_flush(c.get());
478 QVERIFY(windowShownSpy.wait());
479 QVERIFY(window->isShown());
480 }
481
482 // The approaching state will be reset if the window is shown manually.
483 {
484 QSignalSpy approachingSpy(workspace()->screenEdges(), &ScreenEdges::approaching);
485 enableAutoHide(c.get(), windowId, ElectricBottom);
486 xcb_flush(c.get());
487 QVERIFY(windowHiddenSpy.wait());
488 QVERIFY(!window->isShown());
489
490 Test::pointerMotion(QPointF(640, 1020), timestamp++);
491 QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
492 Test::pointerMotion(QPointF(640, 1021), timestamp++);
493 QVERIFY(approachingSpy.last().at(1).toReal() != 0.0);
494
495 enableAutoHide(c.get(), windowId, ElectricNone);
496 xcb_flush(c.get());
497 QVERIFY(windowShownSpy.wait());
498 QVERIFY(window->isShown());
499 QVERIFY(approachingSpy.last().at(1).toReal() == 0.0);
500
501 Test::pointerMotion(QPointF(640, 512), timestamp++);
502 }
503}
504
505} // namespace KWin
506
508#include "screenedges_test.moc"
Xcb::Atom wm_client_leader
Definition atoms.h:30
Xcb::Atom kde_screen_edge_show
Definition atoms.h:61
static Cursors * self()
Definition cursor.cpp:35
PointerInputRedirection * pointer() const
Definition input.h:220
void warp(const QPointF &pos)
ScreenEdgePropertyMonitor(xcb_connection_t *c, xcb_window_t window)
void approaching(ElectricBorder border, qreal factor, const QRect &geometry)
void reserve(ElectricBorder border, QObject *object, const char *callback)
void setConfig(KSharedConfig::Ptr config)
Definition screenedge.h:534
void gotCallback(KWin::ElectricBorder)
bool callback(ElectricBorder border)
void windowShown(KWin::Window *window)
void windowHidden(KWin::Window *window)
ScreenEdges * screenEdges() const
static Workspace * self()
Definition workspace.h:91
void windowAdded(KWin::Window *)
void setActiveOutput(Output *output)
void slotReconfigure()
Q_DECLARE_METATYPE(KWin::SwitchEvent::State)
#define WAYLANDTEST_MAIN(TestObject)
void destroyWaylandConnection()
void setOutputConfig(const QList< QRect > &geometries)
void touchDown(qint32 id, const QPointF &pos, quint32 time)
bool setupWaylandConnection(AdditionalWaylandInterfaces flags=AdditionalWaylandInterfaces())
void touchMotion(qint32 id, const QPointF &pos, quint32 time)
XcbConnectionPtr createX11Connection()
void pointerMotion(const QPointF &position, quint32 time)
void touchUp(qint32 id, quint32 time)
std::unique_ptr< xcb_connection_t, XcbConnectionDeleter > XcbConnectionPtr
KWIN_EXPORT xcb_window_t rootWindow()
Definition xcb.h:24
ElectricBorder
Definition globals.h:60
@ ElectricNone
Definition globals.h:70
@ ElectricTopLeft
Definition globals.h:68
@ ElectricBottomLeft
Definition globals.h:66
@ ElectricBottom
Definition globals.h:65
@ ElectricTopRight
Definition globals.h:62
@ ElectricTop
Definition globals.h:61
@ ElectricRight
Definition globals.h:63
@ ElectricBottomRight
Definition globals.h:64
@ ElectricLeft
Definition globals.h:67
WaylandServer * waylandServer()
Workspace * workspace()
Definition workspace.h:830
KWIN_EXPORT xcb_connection_t * connection()
Definition xcb.h:19
InputRedirection * input()
Definition input.h:549
KWIN_EXPORT Atoms * atoms
Definition main.cpp:74