diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -76,6 +76,7 @@ integrationTest(NAME testSceneQPainter SRCS scene_qpainter_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testSceneQPainterShadow SRCS scene_qpainter_shadow_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testStackingOrder SRCS stacking_order_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testDbusInterface SRCS dbus_interface_test.cpp LIBS XCB::ICCCM) if (KWIN_BUILD_ACTIVITIES) integrationTest(NAME testActivities SRCS activities_test.cpp LIBS XCB::ICCCM) diff --git a/autotests/integration/dbus_interface_test.cpp b/autotests/integration/dbus_interface_test.cpp new file mode 100644 --- /dev/null +++ b/autotests/integration/dbus_interface_test.cpp @@ -0,0 +1,413 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Martin Flöser + +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) version 3 or any later version +accepted by the membership of KDE e.V. (or its successor approved +by the membership of KDE e.V.), which shall act as a proxy +defined in Section 14 of version 3 of the license. + +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 "atoms.h" +#include "client.h" +#include "deleted.h" +#include "platform.h" +#include "rules.h" +#include "screens.h" +#include "shell_client.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#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_dbus_interface-0"); + +const QString s_destination{QStringLiteral("org.kde.KWin")}; +const QString s_path{QStringLiteral("/KWin")}; +const QString s_interface{QStringLiteral("org.kde.KWin")}; + +class TestDbusInterface : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testGetWindowInfoInvalidUuid(); + void testGetWindowInfoShellClient_data(); + void testGetWindowInfoShellClient(); + void testGetWindowInfoX11Client(); +}; + +void TestDbusInterface::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); + VirtualDesktopManager::self()->setCount(4); +} + +void TestDbusInterface::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void TestDbusInterface::cleanup() +{ + Test::destroyWaylandConnection(); +} + +namespace { +QDBusPendingCall getWindowInfo(const QUuid &uuid) +{ + auto msg = QDBusMessage::createMethodCall(s_destination, s_path, s_interface, QStringLiteral("getWindowInfo")); + msg.setArguments({uuid.toString()}); + return QDBusConnection::sessionBus().asyncCall(msg); +} +} + +void TestDbusInterface::testGetWindowInfoInvalidUuid() +{ + QDBusPendingReply reply{getWindowInfo(QUuid::createUuid())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + const auto windowData = reply.value(); + QVERIFY(windowData.empty()); +} + +void TestDbusInterface::testGetWindowInfoShellClient_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("xdgWmBase") << Test::ShellSurfaceType::XdgShellStable; +} + +void TestDbusInterface::testGetWindowInfoShellClient() +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + if (type != Test::ShellSurfaceType::WlShell) { + qobject_cast(shellSurface.data())->setAppId(QByteArrayLiteral("org.kde.foo")); + qobject_cast(shellSurface.data())->setTitle(QStringLiteral("Test window")); + } + + // now let's render + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.isEmpty()); + QVERIFY(clientAddedSpy.wait()); + auto client = clientAddedSpy.first().first().value(); + QVERIFY(client); + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + QVERIFY(!windowData.isEmpty()); + QCOMPARE(windowData.size(), 23); + QCOMPARE(windowData.value(QStringLiteral("type")).toInt(), NET::Normal); + QCOMPARE(windowData.value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(windowData.value(QStringLiteral("y")).toInt(), client->y()); + QCOMPARE(windowData.value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(windowData.value(QStringLiteral("height")).toInt(), client->height()); + QCOMPARE(windowData.value(QStringLiteral("x11DesktopNumber")).toInt(), 1); + QCOMPARE(windowData.value(QStringLiteral("minimized")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("shaded")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("fullscreen")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepAbove")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepBelow")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipTaskbar")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipPager")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipSwitcher")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeHorizontal")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeVertical")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("noBorder")).toBool(), true); + QCOMPARE(windowData.value(QStringLiteral("clientMachine")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("role")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("resourceName")).toString(), QStringLiteral("testDbusInterface")); + if (type == Test::ShellSurfaceType::WlShell) { + QCOMPARE(windowData.value(QStringLiteral("resourceClass")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("desktopFile")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("caption")).toString(), QString()); + } else { + QCOMPARE(windowData.value(QStringLiteral("resourceClass")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("desktopFile")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("caption")).toString(), QStringLiteral("Test window")); + } + + auto verifyProperty = [client] (const QString &name) { + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!client->isMinimized()); + client->setMinimized(true); + QVERIFY(client->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!client->keepAbove()); + client->setKeepAbove(true); + QVERIFY(client->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!client->keepBelow()); + client->setKeepBelow(true); + QVERIFY(client->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!client->skipTaskbar()); + client->setSkipTaskbar(true); + QVERIFY(client->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!client->skipPager()); + client->setSkipPager(true); + QVERIFY(client->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!client->skipSwitcher()); + client->setSkipSwitcher(true); + QVERIFY(client->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + // not testing shaded as that's X11 + // not testing fullscreen, maximizeHorizontal, maximizeVertical and noBorder as those require window geometry changes + + QCOMPARE(client->desktop(), 1); + workspace()->sendClientToDesktop(client, 2, false); + QCOMPARE(client->desktop(), 2); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("x11DesktopNumber")).toInt(), 2); + + client->move(10, 20); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(reply.value().value(QStringLiteral("y")).toInt(), client->y()); + // not testing width, height as that would require window geometry change + + // finally close window + const auto id = client->internalId(); + QSignalSpy windowClosedSpy(client, &ShellClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QCOMPARE(windowClosedSpy.count(), 1); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +} + + +struct XcbConnectionDeleter +{ + static inline void cleanup(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void TestDbusInterface::testGetWindowInfoX11Client() +{ + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 600, 400); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(hints)); + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + xcb_icccm_set_wm_class(c.data(), w, 7, "foo\0bar"); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setName("Some caption"); + winInfo.setDesktopFileName("org.kde.foo"); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QCOMPARE(client->clientSize(), windowGeometry.size()); + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + QVERIFY(!windowData.isEmpty()); + QCOMPARE(windowData.size(), 23); + QCOMPARE(windowData.value(QStringLiteral("type")).toInt(), NET::Normal); + QCOMPARE(windowData.value(QStringLiteral("x")).toInt(), client->x()); + QCOMPARE(windowData.value(QStringLiteral("y")).toInt(), client->y()); + QCOMPARE(windowData.value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(windowData.value(QStringLiteral("height")).toInt(), client->height()); + QCOMPARE(windowData.value(QStringLiteral("x11DesktopNumber")).toInt(), 1); + QCOMPARE(windowData.value(QStringLiteral("minimized")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("shaded")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("fullscreen")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepAbove")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("keepBelow")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipTaskbar")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipPager")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("skipSwitcher")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeHorizontal")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("maximizeVertical")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("noBorder")).toBool(), false); + QCOMPARE(windowData.value(QStringLiteral("role")).toString(), QString()); + QCOMPARE(windowData.value(QStringLiteral("resourceName")).toString(), QStringLiteral("foo")); + QCOMPARE(windowData.value(QStringLiteral("resourceClass")).toString(), QStringLiteral("bar")); + QCOMPARE(windowData.value(QStringLiteral("desktopFile")).toString(), QStringLiteral("org.kde.foo")); + QCOMPARE(windowData.value(QStringLiteral("caption")).toString(), QStringLiteral("Some caption")); + // not testing clientmachine as that is system dependent + + auto verifyProperty = [client] (const QString &name) { + QDBusPendingReply reply{getWindowInfo(client->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!client->isMinimized()); + client->setMinimized(true); + QVERIFY(client->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!client->keepAbove()); + client->setKeepAbove(true); + QVERIFY(client->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!client->keepBelow()); + client->setKeepBelow(true); + QVERIFY(client->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!client->skipTaskbar()); + client->setSkipTaskbar(true); + QVERIFY(client->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!client->skipPager()); + client->setSkipPager(true); + QVERIFY(client->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!client->skipSwitcher()); + client->setSkipSwitcher(true); + QVERIFY(client->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + QVERIFY(!client->isShade()); + client->setShade(ShadeNormal); + QVERIFY(client->isShade()); + QCOMPARE(verifyProperty(QStringLiteral("shaded")), true); + client->setShade(ShadeNone); + QVERIFY(!client->isShade()); + + QVERIFY(!client->noBorder()); + client->setNoBorder(true); + QVERIFY(client->noBorder()); + QCOMPARE(verifyProperty(QStringLiteral("noBorder")), true); + client->setNoBorder(false); + QVERIFY(!client->noBorder()); + + QVERIFY(!client->isFullScreen()); + client->setFullScreen(true); + QVERIFY(client->isFullScreen()); + QVERIFY(client->clientSize() != windowGeometry.size()); + QCOMPARE(verifyProperty(QStringLiteral("fullscreen")), true); + reply = getWindowInfo(client->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("width")).toInt(), client->width()); + QCOMPARE(reply.value().value(QStringLiteral("height")).toInt(), client->height()); + client->setFullScreen(false); + QVERIFY(!client->isFullScreen()); + + // maximize + client->setMaximize(true, false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), false); + client->setMaximize(false, true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), true); + + const auto id = client->internalId(); + // destroy the window + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +} + +WAYLANDTEST_MAIN(TestDbusInterface) +#include "dbus_interface_test.moc" diff --git a/dbusinterface.h b/dbusinterface.h --- a/dbusinterface.h +++ b/dbusinterface.h @@ -70,6 +70,7 @@ Q_NOREPLY void showDebugConsole(); QVariantMap queryWindowInfo(); + QVariantMap getWindowInfo(const QString &uuid); private Q_SLOTS: void becomeKWinService(const QString &service); diff --git a/dbusinterface.cpp b/dbusinterface.cpp --- a/dbusinterface.cpp +++ b/dbusinterface.cpp @@ -189,47 +189,64 @@ console->show(); } +namespace { +QVariantMap clientToVariantMap(const AbstractClient *c) +{ + return { + {QStringLiteral("resourceClass"), c->resourceClass()}, + {QStringLiteral("resourceName"), c->resourceName()}, + {QStringLiteral("desktopFile"), c->desktopFileName()}, + {QStringLiteral("role"), c->windowRole()}, + {QStringLiteral("caption"), c->captionNormal()}, + {QStringLiteral("clientMachine"), c->wmClientMachine(true)}, + {QStringLiteral("type"), c->windowType()}, + {QStringLiteral("x"), c->x()}, + {QStringLiteral("y"), c->y()}, + {QStringLiteral("width"), c->width()}, + {QStringLiteral("height"), c->height()}, + {QStringLiteral("x11DesktopNumber"), c->desktop()}, + {QStringLiteral("minimized"), c->isMinimized()}, + {QStringLiteral("shaded"), c->isShade()}, + {QStringLiteral("fullscreen"), c->isFullScreen()}, + {QStringLiteral("keepAbove"), c->keepAbove()}, + {QStringLiteral("keepBelow"), c->keepBelow()}, + {QStringLiteral("noBorder"), c->noBorder()}, + {QStringLiteral("skipTaskbar"), c->skipTaskbar()}, + {QStringLiteral("skipPager"), c->skipPager()}, + {QStringLiteral("skipSwitcher"), c->skipSwitcher()}, + {QStringLiteral("maximizeHorizontal"), c->maximizeMode() & MaximizeHorizontal}, + {QStringLiteral("maximizeVertical"), c->maximizeMode() & MaximizeVertical} + }; +} +} + QVariantMap DBusInterface::queryWindowInfo() { m_replyQueryWindowInfo = message(); setDelayedReply(true); kwinApp()->platform()->startInteractiveWindowSelection( [this] (Toplevel *t) { if (auto c = qobject_cast(t)) { - const QVariantMap ret{ - {QStringLiteral("resourceClass"), c->resourceClass()}, - {QStringLiteral("resourceName"), c->resourceName()}, - {QStringLiteral("desktopFile"), c->desktopFileName()}, - {QStringLiteral("role"), c->windowRole()}, - {QStringLiteral("caption"), c->captionNormal()}, - {QStringLiteral("clientMachine"), c->wmClientMachine(true)}, - {QStringLiteral("type"), c->windowType()}, - {QStringLiteral("x"), c->x()}, - {QStringLiteral("y"), c->y()}, - {QStringLiteral("width"), c->width()}, - {QStringLiteral("height"), c->height()}, - {QStringLiteral("x11DesktopNumber"), c->desktop()}, - {QStringLiteral("minimized"), c->isMinimized()}, - {QStringLiteral("shaded"), c->isShade()}, - {QStringLiteral("fullscreen"), c->isFullScreen()}, - {QStringLiteral("keepAbove"), c->keepAbove()}, - {QStringLiteral("keepBelow"), c->keepBelow()}, - {QStringLiteral("noBorder"), c->noBorder()}, - {QStringLiteral("skipTaskbar"), c->skipTaskbar()}, - {QStringLiteral("skipPager"), c->skipPager()}, - {QStringLiteral("skipSwitcher"), c->skipSwitcher()}, - {QStringLiteral("maximizeHorizontal"), c->maximizeMode() & MaximizeHorizontal}, - {QStringLiteral("maximizeVertical"), c->maximizeMode() & MaximizeVertical} - }; - QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createReply(ret)); + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createReply(clientToVariantMap(c))); } else { QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createErrorReply(QString(), QString())); } } ); return QVariantMap{}; } +QVariantMap DBusInterface::getWindowInfo(const QString &uuid) +{ + const auto id = QUuid::fromString(uuid); + const auto client = workspace()->findAbstractClient([&id] (const AbstractClient *c) { return c->internalId() == id; }); + if (client) { + return clientToVariantMap(client); + } else { + return {}; + } +} + CompositorDBusInterface::CompositorDBusInterface(Compositor *parent) : QObject(parent) , m_compositor(parent) diff --git a/org.kde.KWin.xml b/org.kde.KWin.xml --- a/org.kde.KWin.xml +++ b/org.kde.KWin.xml @@ -40,5 +40,10 @@ + + + + +