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