1 /*
2 SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5 */
6
7 #include <QSignalSpy>
8 #include <QX11Info>
9 #include <kmanagerselection.h>
10 #include <kwindoweffects.h>
11 #include <kwindowsystem.h>
12 #include <netwm.h>
13 #include <qtest_widgets.h>
14 #include <xcb/xcb.h>
15
16 Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation)
17 Q_DECLARE_METATYPE(KWindowEffects::Effect)
18
19 class KWindowEffectsTest : public QObject
20 {
21 Q_OBJECT
22 private Q_SLOTS:
23 void initTestCase();
24 void testSlideWindow_data();
25 void testSlideWindow();
26 void testSlideWindowRemove();
27 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
28 void testPresentWindows_data();
29 void testPresentWindows();
30 void testPresentWindowsEmptyGroup();
31 void testPresentWindowsGroup_data();
32 void testPresentWindowsGroup();
33 void testHighlightWindows_data();
34 void testHighlightWindows();
35 void testHighlightWindowsEmpty();
36 #endif
37 void testBlur_data();
38 void testBlur();
39 void testBlurDisable();
40 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 67)
41 void testMarkAsDashboard();
42 #endif
43 void testEffectAvailable_data();
44 void testEffectAvailable();
45
46 private:
47 int32_t locationToValue(KWindowEffects::SlideFromLocation location) const;
48 void performSlideWindowTest(xcb_window_t window, int offset, KWindowEffects::SlideFromLocation location) const;
49 void performSlideWindowRemoveTest(xcb_window_t window);
50 void performWindowsOnPropertyTest(xcb_atom_t atom, const QList<WId> &windows);
51 void performAtomIsRemoveTest(xcb_window_t window, xcb_atom_t atom);
52 void getHelperAtom(const QByteArray &name, xcb_atom_t *atom) const;
53 xcb_atom_t m_slide;
54 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
55 xcb_atom_t m_presentWindows;
56 xcb_atom_t m_presentWindowsGroup;
57 xcb_atom_t m_highlightWindows;
58 #endif
59 xcb_atom_t m_thumbnails;
60 xcb_atom_t m_blur;
61 QScopedPointer<QWindow> m_window;
62 QScopedPointer<QWidget> m_widget;
63 };
64
initTestCase()65 void KWindowEffectsTest::initTestCase()
66 {
67 m_window.reset(new QWindow());
68 QVERIFY(m_window->winId() != XCB_WINDOW_NONE);
69 m_widget.reset(new QWidget());
70 m_widget->show();
71 QVERIFY(m_widget->effectiveWinId() != XCB_WINDOW_NONE);
72
73 getHelperAtom(QByteArrayLiteral("_KDE_SLIDE"), &m_slide);
74 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
75 getHelperAtom(QByteArrayLiteral("_KDE_PRESENT_WINDOWS_DESKTOP"), &m_presentWindows);
76 getHelperAtom(QByteArrayLiteral("_KDE_PRESENT_WINDOWS_GROUP"), &m_presentWindowsGroup);
77 getHelperAtom(QByteArrayLiteral("_KDE_WINDOW_HIGHLIGHT"), &m_highlightWindows);
78 #endif
79 getHelperAtom(QByteArrayLiteral("_KDE_WINDOW_PREVIEW"), &m_thumbnails);
80 getHelperAtom(QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION"), &m_blur);
81 }
82
getHelperAtom(const QByteArray & name,xcb_atom_t * atom) const83 void KWindowEffectsTest::getHelperAtom(const QByteArray &name, xcb_atom_t *atom) const
84 {
85 xcb_connection_t *c = QX11Info::connection();
86 xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(c, false, name.length(), name.constData());
87
88 QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> reply(xcb_intern_atom_reply(c, atomCookie, nullptr));
89 QVERIFY(!reply.isNull());
90 *atom = reply->atom;
91 }
92
testSlideWindow_data()93 void KWindowEffectsTest::testSlideWindow_data()
94 {
95 QTest::addColumn<int>("offset");
96 QTest::addColumn<KWindowEffects::SlideFromLocation>("location");
97
98 QTest::newRow("Left") << 10 << KWindowEffects::LeftEdge;
99 QTest::newRow("Right") << 20 << KWindowEffects::RightEdge;
100 QTest::newRow("Top") << 0 << KWindowEffects::TopEdge;
101 QTest::newRow("Bottom") << -1 << KWindowEffects::BottomEdge;
102 }
103
testSlideWindow()104 void KWindowEffectsTest::testSlideWindow()
105 {
106 QFETCH(int, offset);
107 QFETCH(KWindowEffects::SlideFromLocation, location);
108
109 KWindowEffects::slideWindow(m_window.data(), location, offset);
110 performSlideWindowTest(m_window->winId(), offset, location);
111 }
112
testSlideWindowRemove()113 void KWindowEffectsTest::testSlideWindowRemove()
114 {
115 xcb_window_t window = m_window->winId();
116 // first install the atom
117 KWindowEffects::slideWindow(m_window.data(), KWindowEffects::TopEdge, 0);
118 performSlideWindowTest(window, 0, KWindowEffects::TopEdge);
119
120 // now delete it
121 KWindowEffects::slideWindow(m_window.data(), KWindowEffects::NoEdge, 0);
122 performSlideWindowRemoveTest(window);
123 }
124
performSlideWindowTest(xcb_window_t window,int offset,KWindowEffects::SlideFromLocation location) const125 void KWindowEffectsTest::performSlideWindowTest(xcb_window_t window, int offset, KWindowEffects::SlideFromLocation location) const
126 {
127 xcb_connection_t *c = QX11Info::connection();
128 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, window, m_slide, m_slide, 0, 100);
129 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
130 QVERIFY(!reply.isNull());
131 QCOMPARE(reply->format, uint8_t(32));
132 QCOMPARE(reply->value_len, uint32_t(2));
133 QCOMPARE(reply->type, m_slide);
134 int32_t *data = static_cast<int32_t *>(xcb_get_property_value(reply.data()));
135 QCOMPARE(data[0], offset);
136 QCOMPARE(data[1], locationToValue(location));
137 }
138
performSlideWindowRemoveTest(xcb_window_t window)139 void KWindowEffectsTest::performSlideWindowRemoveTest(xcb_window_t window)
140 {
141 performAtomIsRemoveTest(window, m_slide);
142 }
143
performAtomIsRemoveTest(xcb_window_t window,xcb_atom_t atom)144 void KWindowEffectsTest::performAtomIsRemoveTest(xcb_window_t window, xcb_atom_t atom)
145 {
146 xcb_connection_t *c = QX11Info::connection();
147 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, window, atom, atom, 0, 100);
148 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
149 QVERIFY(!reply.isNull());
150 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_NONE));
151 }
152
locationToValue(KWindowEffects::SlideFromLocation location) const153 int32_t KWindowEffectsTest::locationToValue(KWindowEffects::SlideFromLocation location) const
154 {
155 switch (location) {
156 case KWindowEffects::LeftEdge:
157 return 0;
158 case KWindowEffects::TopEdge:
159 return 1;
160 case KWindowEffects::RightEdge:
161 return 2;
162 case KWindowEffects::BottomEdge:
163 return 3;
164 default:
165 return -1;
166 }
167 }
168
169 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testPresentWindows_data()170 void KWindowEffectsTest::testPresentWindows_data()
171 {
172 QTest::addColumn<int>("desktop");
173
174 QTest::newRow("all desktops") << -1;
175 QTest::newRow("1") << 1;
176 QTest::newRow("2") << 2;
177 QTest::newRow("3") << 3;
178 QTest::newRow("4") << 4;
179 }
180 #endif
181
182 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testPresentWindows()183 void KWindowEffectsTest::testPresentWindows()
184 {
185 QFETCH(int, desktop);
186
187 KWindowEffects::presentWindows(m_window->winId(), desktop);
188
189 xcb_connection_t *c = QX11Info::connection();
190 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), m_presentWindows, m_presentWindows, 0, 100);
191 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
192 QVERIFY(!reply.isNull());
193 QCOMPARE(reply->format, uint8_t(32));
194 QCOMPARE(reply->value_len, uint32_t(1));
195 QCOMPARE(reply->type, m_presentWindows);
196 int32_t *data = static_cast<int32_t *>(xcb_get_property_value(reply.data()));
197 QCOMPARE(data[0], desktop);
198 }
199 #endif
200
201 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testPresentWindowsEmptyGroup()202 void KWindowEffectsTest::testPresentWindowsEmptyGroup()
203 {
204 KWindowEffects::presentWindows(m_window->winId(), QList<WId>());
205
206 xcb_connection_t *c = QX11Info::connection();
207 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), m_presentWindowsGroup, m_presentWindowsGroup, 0, 100);
208 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
209 QVERIFY(!reply.isNull());
210 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_NONE));
211 }
212 #endif
213
214 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testPresentWindowsGroup_data()215 void KWindowEffectsTest::testPresentWindowsGroup_data()
216 {
217 QTest::addColumn<QList<WId>>("windows");
218
219 QTest::newRow("one") << (QList<WId>() << m_window->winId());
220 QTest::newRow("two") << (QList<WId>() << m_window->winId() << m_widget->effectiveWinId());
221 }
222 #endif
223
224 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testPresentWindowsGroup()225 void KWindowEffectsTest::testPresentWindowsGroup()
226 {
227 QFETCH(QList<WId>, windows);
228 KWindowEffects::presentWindows(m_window->winId(), windows);
229 performWindowsOnPropertyTest(m_presentWindowsGroup, windows);
230 }
231 #endif
232
233 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testHighlightWindows_data()234 void KWindowEffectsTest::testHighlightWindows_data()
235 {
236 QTest::addColumn<QList<WId>>("windows");
237
238 QTest::newRow("one") << (QList<WId>() << m_window->winId());
239 QTest::newRow("two") << (QList<WId>() << m_window->winId() << m_widget->effectiveWinId());
240 }
241 #endif
242
243 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testHighlightWindows()244 void KWindowEffectsTest::testHighlightWindows()
245 {
246 QFETCH(QList<WId>, windows);
247 KWindowEffects::highlightWindows(m_window->winId(), windows);
248 performWindowsOnPropertyTest(m_highlightWindows, windows);
249 }
250 #endif
251
252 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
testHighlightWindowsEmpty()253 void KWindowEffectsTest::testHighlightWindowsEmpty()
254 {
255 // ensure it's empty
256 KWindowEffects::highlightWindows(m_window->winId(), QList<WId>());
257 performAtomIsRemoveTest(m_window->winId(), m_highlightWindows);
258
259 // install some windows on the atom
260 QList<WId> windows;
261 windows.append(m_window->winId());
262 windows.append(m_widget->effectiveWinId());
263 KWindowEffects::highlightWindows(m_window->winId(), windows);
264 performWindowsOnPropertyTest(m_highlightWindows, windows);
265
266 // and remove it again
267 KWindowEffects::highlightWindows(m_window->winId(), QList<WId>());
268 performAtomIsRemoveTest(m_window->winId(), m_highlightWindows);
269 }
270 #endif
271
performWindowsOnPropertyTest(xcb_atom_t atom,const QList<WId> & windows)272 void KWindowEffectsTest::performWindowsOnPropertyTest(xcb_atom_t atom, const QList<WId> &windows)
273 {
274 xcb_connection_t *c = QX11Info::connection();
275 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), atom, atom, 0, 100);
276 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
277 QVERIFY(!reply.isNull());
278 QCOMPARE(reply->type, atom);
279 QCOMPARE(reply->format, uint8_t(32));
280 QCOMPARE(reply->value_len, uint32_t(windows.size()));
281 int32_t *data = static_cast<int32_t *>(xcb_get_property_value(reply.data()));
282 for (int i = 0; i < windows.size(); ++i) {
283 QCOMPARE(data[i], int32_t(windows.at(i)));
284 }
285 }
286
testBlur_data()287 void KWindowEffectsTest::testBlur_data()
288 {
289 QTest::addColumn<QRegion>("blur");
290
291 QRegion region(0, 0, 10, 10);
292 QTest::newRow("one rect") << region;
293 region = region.united(QRect(20, 20, 5, 5));
294 QTest::newRow("two rects") << region;
295 region = region.united(QRect(100, 100, 20, 20));
296 QTest::newRow("three rects") << region;
297 QTest::newRow("empty") << QRegion();
298 }
299
testBlur()300 void KWindowEffectsTest::testBlur()
301 {
302 QFETCH(QRegion, blur);
303
304 KWindowEffects::enableBlurBehind(m_window.data(), true, blur);
305 xcb_connection_t *c = QX11Info::connection();
306 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), m_blur, XCB_ATOM_CARDINAL, 0, 100);
307 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
308 QVERIFY(!reply.isNull());
309 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_CARDINAL));
310 QCOMPARE(reply->format, uint8_t(32));
311 QCOMPARE(reply->value_len, uint32_t(blur.rectCount() * 4));
312 uint32_t *data = static_cast<uint32_t *>(xcb_get_property_value(reply.data()));
313 int dataOffset = 0;
314 for (const QRect &rect : blur) {
315 QCOMPARE(data[dataOffset++], uint32_t(rect.x()));
316 QCOMPARE(data[dataOffset++], uint32_t(rect.y()));
317 QCOMPARE(data[dataOffset++], uint32_t(rect.width()));
318 QCOMPARE(data[dataOffset++], uint32_t(rect.height()));
319 }
320 }
321
testBlurDisable()322 void KWindowEffectsTest::testBlurDisable()
323 {
324 KWindowEffects::enableBlurBehind(m_window.data(), false);
325 performAtomIsRemoveTest(m_window->winId(), m_blur);
326
327 KWindowEffects::enableBlurBehind(m_window.data(), true);
328 // verify that it got added
329 xcb_connection_t *c = QX11Info::connection();
330 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), m_blur, XCB_ATOM_CARDINAL, 0, 100);
331 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
332 QVERIFY(!reply.isNull());
333 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_CARDINAL));
334
335 // and disable
336 KWindowEffects::enableBlurBehind(m_window.data(), false);
337 performAtomIsRemoveTest(m_window->winId(), m_blur);
338 }
339
340 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 67)
testMarkAsDashboard()341 void KWindowEffectsTest::testMarkAsDashboard()
342 {
343 const QByteArray className = QByteArrayLiteral("dashboard");
344 // should not yet be set
345 xcb_connection_t *c = QX11Info::connection();
346 xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(c, false, m_window->winId(), XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0, 100);
347 QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> reply(xcb_get_property_reply(c, cookie, nullptr));
348 QVERIFY(!reply.isNull());
349 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_STRING));
350 QCOMPARE(reply->format, uint8_t(8));
351 char *data = static_cast<char *>(xcb_get_property_value(reply.data()));
352 QVERIFY(QByteArray(data) != className);
353
354 // now mark as dashboard
355 KWindowEffects::markAsDashboard(m_window->winId());
356 cookie = xcb_get_property_unchecked(c, false, m_window->winId(), XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0, 100);
357 reply.reset(xcb_get_property_reply(c, cookie, nullptr));
358 QVERIFY(!reply.isNull());
359 QCOMPARE(reply->type, xcb_atom_t(XCB_ATOM_STRING));
360 QCOMPARE(reply->format, uint8_t(8));
361 QCOMPARE(reply->value_len, uint32_t(19));
362 data = static_cast<char *>(xcb_get_property_value(reply.data()));
363 QCOMPARE(QByteArray(data), className);
364 data = data + 10;
365 QCOMPARE(QByteArray(data), className);
366 }
367 #endif
368
testEffectAvailable_data()369 void KWindowEffectsTest::testEffectAvailable_data()
370 {
371 QTest::addColumn<KWindowEffects::Effect>("effect");
372 QTest::addColumn<QByteArray>("propertyName");
373
374 QTest::newRow("slide") << KWindowEffects::Slide << QByteArrayLiteral("_KDE_SLIDE");
375 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 82)
376 QTest::newRow("PresentWindows") << KWindowEffects::PresentWindows << QByteArrayLiteral("_KDE_PRESENT_WINDOWS_DESKTOP");
377 QTest::newRow("PresentWindowsGroup") << KWindowEffects::PresentWindowsGroup << QByteArrayLiteral("_KDE_PRESENT_WINDOWS_GROUP");
378 QTest::newRow("HighlightWindows") << KWindowEffects::HighlightWindows << QByteArrayLiteral("_KDE_WINDOW_HIGHLIGHT");
379 #endif
380 QTest::newRow("BlurBehind") << KWindowEffects::BlurBehind << QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION");
381 #if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 67)
382 QTest::newRow("Dashboard") << KWindowEffects::Dashboard << QByteArrayLiteral("_WM_EFFECT_KDE_DASHBOARD");
383 #endif
384 QTest::newRow("BackgroundContrast") << KWindowEffects::BackgroundContrast << QByteArrayLiteral("_KDE_NET_WM_BACKGROUND_CONTRAST_REGION");
385 }
386
testEffectAvailable()387 void KWindowEffectsTest::testEffectAvailable()
388 {
389 NETRootInfo rootInfo(QX11Info::connection(), NET::Supported | NET::SupportingWMCheck);
390 if (qstrcmp(rootInfo.wmName(), "KWin") == 0) {
391 QSKIP("KWin running, we don't want to interact with the running system");
392 }
393 // this test verifies whether an effect is available
394 QFETCH(KWindowEffects::Effect, effect);
395 // without a compositing manager it's not available
396 // try-verify as there still might be the selection claimed from previous data run
397 QTRY_VERIFY(!KWindowSystem::compositingActive());
398 QVERIFY(!KWindowEffects::isEffectAvailable(effect));
399
400 // fake the compositor
401 QSignalSpy compositingChangedSpy(KWindowSystem::self(), &KWindowSystem::compositingChanged);
402 QVERIFY(compositingChangedSpy.isValid());
403 KSelectionOwner compositorSelection("_NET_WM_CM_S0");
404 QSignalSpy claimedSpy(&compositorSelection, &KSelectionOwner::claimedOwnership);
405 QVERIFY(claimedSpy.isValid());
406 compositorSelection.claim(true);
407 QVERIFY(claimedSpy.wait());
408 QCOMPARE(compositingChangedSpy.count(), 1);
409 QCOMPARE(compositingChangedSpy.first().first().toBool(), true);
410 QVERIFY(KWindowSystem::compositingActive());
411
412 // but not yet available
413 QVERIFY(!KWindowEffects::isEffectAvailable(effect));
414
415 // set the atom
416 QFETCH(QByteArray, propertyName);
417 xcb_connection_t *c = QX11Info::connection();
418 xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(c, false, propertyName.length(), propertyName.constData());
419 QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> atom(xcb_intern_atom_reply(c, atomCookie, nullptr));
420 QVERIFY(!atom.isNull());
421 unsigned char dummy = 0;
422 xcb_change_property(c, XCB_PROP_MODE_REPLACE, QX11Info::appRootWindow(), atom->atom, atom->atom, 8, 1, &dummy);
423 xcb_flush(c);
424
425 // now the effect should be available
426 QVERIFY(KWindowEffects::isEffectAvailable(effect));
427
428 // delete the property again
429 xcb_delete_property(c, QX11Info::appRootWindow(), atom->atom);
430 xcb_flush(c);
431 // which means it's no longer available
432 QVERIFY(!KWindowEffects::isEffectAvailable(effect));
433
434 // remove compositing selection
435 compositorSelection.release();
436 QVERIFY(compositingChangedSpy.wait());
437 QCOMPARE(compositingChangedSpy.count(), 2);
438 QCOMPARE(compositingChangedSpy.last().first().toBool(), false);
439 QVERIFY(!KWindowSystem::compositingActive());
440 }
441
442 QTEST_MAIN(KWindowEffectsTest)
443
444 #include "kwindoweffectstest.moc"
445