diff --git a/CMakeLists.txt b/CMakeLists.txt index eea67168ed..6fedcfeff5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -321,6 +321,7 @@ set(kwin_KDEINIT_SRCS killwindow.cpp geometrytip.cpp screens.cpp + screens_xrandr.cpp shadow.cpp sm.cpp group.cpp diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index e76359d167..51e8bda6a5 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -189,6 +189,7 @@ set( testScreens_SRCS mock_screens.cpp mock_workspace.cpp ../screens.cpp + ../x11eventfilter.cpp ) kconfig_add_kcfg_files(testScreens_SRCS ../settings.kcfgc) @@ -206,6 +207,41 @@ target_link_libraries(testScreens add_test(kwin_testScreens testScreens) ecm_mark_as_test(testScreens) +######################################################## +# Test XrandRScreens +######################################################## +set( testXRandRScreens_SRCS + test_xrandr_screens.cpp + mock_client.cpp + mock_screens.cpp + mock_workspace.cpp + ../screens.cpp + ../screens_xrandr.cpp + ../xcbutils.cpp # init of extensions + ../x11eventfilter.cpp +) +kconfig_add_kcfg_files(testXRandRScreens_SRCS ../settings.kcfgc) +add_executable( testXRandRScreens ${testXRandRScreens_SRCS} ) +target_link_libraries( testXRandRScreens + Qt5::Test + Qt5::Gui + KF5::ConfigCore + KF5::ConfigGui + KF5::WindowSystem + KF5::Service + XCB::XCB + XCB::RANDR + XCB::XFIXES + XCB::SYNC + XCB::COMPOSITE + XCB::DAMAGE + XCB::GLX + XCB::SHM +) + +add_test(kwin-testXRandRScreens testXRandRScreens) +ecm_mark_as_test(testXRandRScreens) + ######################################################## # Test ScreenEdges ######################################################## diff --git a/autotests/mock_workspace.cpp b/autotests/mock_workspace.cpp index 05a46cf108..1373b21f6c 100644 --- a/autotests/mock_workspace.cpp +++ b/autotests/mock_workspace.cpp @@ -76,5 +76,15 @@ QRect MockWorkspace::clientArea(clientAreaOption, int screen, int desktop) const return QRect(); } +void MockWorkspace::registerEventFilter(X11EventFilter *filter) +{ + Q_UNUSED(filter) +} + +void MockWorkspace::unregisterEventFilter(X11EventFilter *filter) +{ + Q_UNUSED(filter) +} + } diff --git a/autotests/mock_workspace.h b/autotests/mock_workspace.h index 40a770c542..f1ce913dd1 100644 --- a/autotests/mock_workspace.h +++ b/autotests/mock_workspace.h @@ -27,6 +27,7 @@ namespace KWin { class Client; +class X11EventFilter; class MockWorkspace; typedef MockWorkspace Workspace; @@ -46,6 +47,9 @@ public: void setActiveClient(Client *c); void setMovingClient(Client *c); + void registerEventFilter(X11EventFilter *filter); + void unregisterEventFilter(X11EventFilter *filter); + static Workspace *self(); Q_SIGNALS: diff --git a/autotests/test_xrandr_screens.cpp b/autotests/test_xrandr_screens.cpp new file mode 100644 index 0000000000..5bd3c5b98c --- /dev/null +++ b/autotests/test_xrandr_screens.cpp @@ -0,0 +1,281 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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) any later version. + +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 "../screens_xrandr.h" +#include "../cursor.h" +#include "../xcbutils.h" +#include "mock_workspace.h" +// Qt +#include + +// mocking +namespace KWin +{ + +QPoint Cursor::pos() +{ + return QPoint(0, 0); +} +} // namespace KWin + +static xcb_window_t s_rootWindow = XCB_WINDOW_NONE; +static xcb_connection_t *s_connection = nullptr; + +unsigned long QX11Info::appRootWindow(int) +{ + return s_rootWindow; +} +xcb_connection_t *QX11Info::connection() +{ + return s_connection; +} + +using namespace KWin; +using namespace KWin::Xcb; + +class TestXRandRScreens : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void testStartup(); + void testChange(); + void testMultipleChanges(); +private: + QScopedPointer m_xserver; +}; + +void TestXRandRScreens::initTestCase() +{ + // TODO: turn into init instead of initTestCase + // needs to be initTestCase as KWin::connection caches the first created xcb_connection_t + // thus changing X server for each test run would create problems + qsrand(QDateTime::currentMSecsSinceEpoch()); + // first reset just to be sure + s_connection = nullptr; + s_rootWindow = XCB_WINDOW_NONE; + // start X Server + m_xserver.reset(new QProcess); + // randomize the display id in [1, 98] + // 0 is not used because it conflicts with "normal" X server + // 99 is not used because it's used by KDE's CI infrastructure + const QString id = QStringLiteral(":") + QString::number((qrand() % 98) + 1); + // using Xephyr as Xvfb doesn't support render extension + m_xserver->start(QStringLiteral("Xephyr"), QStringList() << id); + QVERIFY(m_xserver->waitForStarted()); + QCOMPARE(m_xserver->state(), QProcess::Running); + // give it some time before we open the X Display + QTest::qWait(100); + // create X connection + int screen = 0; + s_connection = xcb_connect(qPrintable(id), &screen); + QVERIFY(s_connection); + + // set root window + xcb_screen_iterator_t iter = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); + for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); + it.rem; + --screen, xcb_screen_next(&it)) { + if (screen == 0) { + s_rootWindow = iter.data->root; + break; + } + } + QVERIFY(s_rootWindow != XCB_WINDOW_NONE); + + // get the extensions + if (!Extensions::self()->isRandrAvailable()) { + QSKIP("XRandR extension required"); + } + for (const auto &extension : Extensions::self()->extensions()) { + if (extension.name == QByteArrayLiteral("RANDR")) { + if (extension.version < 1 * 0x10 + 4) { + QSKIP("At least XRandR 1.4 required"); + } + } + } +} + +void TestXRandRScreens::cleanupTestCase() +{ + Extensions::destroy(); + // close connection + xcb_disconnect(s_connection); + s_connection = nullptr; + s_rootWindow = XCB_WINDOW_NONE; + // kill X + m_xserver->terminate(); + m_xserver->waitForFinished(); +} + +void TestXRandRScreens::testStartup() +{ + KWin::MockWorkspace ws; + QScopedPointer screens(new XRandRScreens(this)); + QVERIFY(screens->eventType() != 0); + QCOMPARE(screens->eventType(), Xcb::Extensions::self()->randrNotifyEvent()); + QCOMPARE(screens->extension(), 0); + QCOMPARE(screens->genericEventType(), 0); + screens->init(); + QRect xephyrDefault = QRect(0, 0, 640, 480); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), xephyrDefault); + QCOMPARE(screens->geometry(1), QRect()); + QCOMPARE(screens->geometry(-1), QRect()); + QCOMPARE(static_cast(screens.data())->geometry(), xephyrDefault); + QCOMPARE(screens->size(0), xephyrDefault.size()); + QCOMPARE(screens->size(1), QSize()); + QCOMPARE(screens->size(-1), QSize()); + QCOMPARE(static_cast(screens.data())->size(), xephyrDefault.size()); + // unfortunately we only have one output, so let's try at least to test somewhat + QCOMPARE(screens->number(QPoint(0, 0)), 0); + QCOMPARE(screens->number(QPoint(639, 479)), 0); + QCOMPARE(screens->number(QPoint(1280, 1024)), 0); + + // let's change the mode + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter.isNull()); + + // now let's recreate the XRandRScreens + screens.reset(new XRandRScreens(this)); + screens->init(); + QRect geo = QRect(0, 0, modes[0].width, modes[0].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +void TestXRandRScreens::testChange() +{ + KWin::MockWorkspace ws; + QScopedPointer screens(new XRandRScreens(this)); + screens->init(); + + // create some signal spys + QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); + QVERIFY(changedSpy.isValid()); + QVERIFY(changedSpy.isEmpty()); + QVERIFY(changedSpy.wait()); + changedSpy.clear(); + QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); + QVERIFY(geometrySpy.isValid()); + QVERIFY(geometrySpy.isEmpty()); + QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); + QVERIFY(sizeSpy.isValid()); + QVERIFY(sizeSpy.isEmpty()); + + // clear the event loop + while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { + free(e); + } + + // let's change + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + xcb_flush(s_connection); + QVERIFY(!setter.isNull()); + QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); + + xcb_generic_event_t *e = xcb_wait_for_event(s_connection); + screens->event(e); + free(e); + + QVERIFY(changedSpy.wait()); + QCOMPARE(changedSpy.size(), 1); + QCOMPARE(sizeSpy.size(), 1); + QCOMPARE(geometrySpy.size(), 1); + QRect geo = QRect(0, 0, modes[1].width, modes[1].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +void TestXRandRScreens::testMultipleChanges() +{ + KWin::MockWorkspace ws; + // multiple changes should only hit one changed signal + QScopedPointer screens(new XRandRScreens(this)); + screens->init(); + + // create some signal spys + QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); + QVERIFY(changedSpy.isValid()); + QVERIFY(changedSpy.isEmpty()); + QVERIFY(changedSpy.wait()); + changedSpy.clear(); + QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); + QVERIFY(geometrySpy.isValid()); + QVERIFY(geometrySpy.isEmpty()); + QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); + QVERIFY(sizeSpy.isValid()); + QVERIFY(sizeSpy.isEmpty()); + + // clear the event loop + while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { + free(e); + } + + // first change + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter.isNull()); + QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); + // second change + RandR::SetCrtcConfig setter2(crtcs[0], setter->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter2.isNull()); + QVERIFY(setter2->status == XCB_RANDR_SET_CONFIG_SUCCESS); + + auto passEvent = [&screens]() { + xcb_generic_event_t *e = xcb_wait_for_event(s_connection); + screens->event(e); + free(e); + }; + passEvent(); + passEvent(); + + QVERIFY(changedSpy.wait()); + QCOMPARE(changedSpy.size(), 1); + // previous state was modes[1] so the size didn't change + QVERIFY(sizeSpy.isEmpty()); + QVERIFY(geometrySpy.isEmpty()); + QRect geo = QRect(0, 0, modes[1].width, modes[1].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +QTEST_GUILESS_MAIN(TestXRandRScreens) +#include "test_xrandr_screens.moc" diff --git a/screens.cpp b/screens.cpp index 5b9cd34aea..b1a4201984 100644 --- a/screens.cpp +++ b/screens.cpp @@ -23,6 +23,7 @@ along with this program. If not, see . #include "settings.h" #include #include +#include "screens_xrandr.h" #if HAVE_WAYLAND #include "screens_wayland.h" #endif @@ -30,10 +31,6 @@ along with this program. If not, see . #include #endif -#include -#include -#include - namespace KWin { @@ -50,7 +47,7 @@ Screens *Screens::create(QObject *parent) } #endif if (kwinApp()->operationMode() == Application::OperationModeX11) { - s_self = new DesktopWidgetScreens(parent); + s_self = new XRandRScreens(parent); } #endif s_self->init(); @@ -173,46 +170,4 @@ int Screens::intersecting(const QRect &r) const return cnt; } -DesktopWidgetScreens::DesktopWidgetScreens(QObject *parent) - : Screens(parent) - , m_desktop(QApplication::desktop()) -{ -} - -DesktopWidgetScreens::~DesktopWidgetScreens() -{ -} - -void DesktopWidgetScreens::init() -{ - Screens::init(); - connect(m_desktop, SIGNAL(screenCountChanged(int)), SLOT(startChangedTimer())); - connect(m_desktop, SIGNAL(resized(int)), SLOT(startChangedTimer())); - updateCount(); -} - -QRect DesktopWidgetScreens::geometry(int screen) const -{ - if (Screens::self()->isChanging()) - const_cast(this)->updateCount(); - return m_desktop->screenGeometry(screen); -} - -QSize DesktopWidgetScreens::size(int screen) const -{ - return geometry(screen).size(); -} - -int DesktopWidgetScreens::number(const QPoint &pos) const -{ - if (Screens::self()->isChanging()) - const_cast(this)->updateCount(); - return m_desktop->screenNumber(pos); -} - -void DesktopWidgetScreens::updateCount() -{ - setCount(m_desktop->screenCount()); -} - } // namespace diff --git a/screens.h b/screens.h index 25fed8052f..0d8369e337 100644 --- a/screens.h +++ b/screens.h @@ -30,9 +30,6 @@ along with this program. If not, see . #include #include - -class QDesktopWidget; - namespace KWin { class Client; @@ -143,23 +140,6 @@ private: KWIN_SINGLETON(Screens) }; -class DesktopWidgetScreens : public Screens -{ - Q_OBJECT -public: - DesktopWidgetScreens(QObject *parent); - virtual ~DesktopWidgetScreens(); - void init() override; - virtual QRect geometry(int screen) const; - virtual int number(const QPoint &pos) const; - QSize size(int screen) const override; -protected Q_SLOTS: - void updateCount(); - -private: - QDesktopWidget *m_desktop; -}; - inline void Screens::setConfig(KSharedConfig::Ptr config) { diff --git a/screens_xrandr.cpp b/screens_xrandr.cpp new file mode 100644 index 0000000000..dd8f809f17 --- /dev/null +++ b/screens_xrandr.cpp @@ -0,0 +1,128 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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) any later version. + +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 "screens_xrandr.h" +#include "xcbutils.h" + +namespace KWin +{ + +XRandRScreens::XRandRScreens(QObject *parent) + : Screens(parent) + , X11EventFilter(Xcb::Extensions::self()->randrNotifyEvent()) +{ +} + +XRandRScreens::~XRandRScreens() = default; + +template +void XRandRScreens::update() +{ + auto fallback = [this]() { + m_geometries << QRect(); + setCount(1); + }; + m_geometries.clear(); + T resources(rootWindow()); + if (resources.isNull()) { + fallback(); + return; + } + xcb_randr_crtc_t *crtcs = resources.crtcs(); + + QVector infos(resources->num_crtcs); + for (int i = 0; i < resources->num_crtcs; ++i) { + infos[i] = Xcb::RandR::CrtcInfo(crtcs[i], resources->config_timestamp); + } + for (int i = 0; i < resources->num_crtcs; ++i) { + Xcb::RandR::CrtcInfo info(infos.at(i)); + const QRect geo = info.rect(); + if (geo.isValid()) { + m_geometries << geo; + } + } + if (m_geometries.isEmpty()) { + fallback(); + return; + } + + setCount(m_geometries.count()); +} + + +void XRandRScreens::init() +{ + KWin::Screens::init(); + // we need to call ScreenResources at least once to be able to use current + update(); + emit changed(); +} + +QRect XRandRScreens::geometry(int screen) const +{ + if (screen >= m_geometries.size() || screen < 0) { + return QRect(); + } + return m_geometries.at(screen); +} + +int XRandRScreens::number(const QPoint &pos) const +{ + int bestScreen = 0; + int minDistance = INT_MAX; + for (int i = 0; i < m_geometries.size(); ++i) { + const QRect &geo = m_geometries.at(i); + if (geo.contains(pos)) { + return i; + } + int distance = QPoint(geo.topLeft() - pos).manhattanLength(); + distance = qMin(distance, QPoint(geo.topRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomRight() - pos).manhattanLength()); + distance = qMin(distance, QPoint(geo.bottomLeft() - pos).manhattanLength()); + if (distance < minDistance) { + minDistance = distance; + bestScreen = i; + } + } + return bestScreen; +} + +QSize XRandRScreens::size(int screen) const +{ + const QRect geo = geometry(screen); + if (!geo.isValid()) { + return QSize(); + } + return geo.size(); +} + +void XRandRScreens::updateCount() +{ + update(); +} + +bool XRandRScreens::event(xcb_generic_event_t *event) +{ + Q_ASSERT((event->response_type & ~0x80) == Xcb::Extensions::self()->randrNotifyEvent()); + // let's try to gather a few XRandR events, unlikely that there is just one + startChangedTimer(); + return false; +} + +} // namespace diff --git a/screens_xrandr.h b/screens_xrandr.h new file mode 100644 index 0000000000..ed96241efe --- /dev/null +++ b/screens_xrandr.h @@ -0,0 +1,56 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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) any later version. + +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 . +*********************************************************************/ +#ifndef KWIN_SCREENS_XRANDR_H +#define KWIN_SCREENS_XRANDR_H +// kwin +#include "screens.h" +#include "x11eventfilter.h" +// Qt +#include + +namespace KWin +{ + +class XRandRScreens : public Screens, public X11EventFilter +{ + Q_OBJECT +public: + XRandRScreens(QObject *parent); + virtual ~XRandRScreens(); + void init() override; + QRect geometry(int screen) const override; + int number(const QPoint& pos) const override; + QSize size(int screen) const override; + + using QObject::event; + bool event(xcb_generic_event_t *event) override; + +protected Q_SLOTS: + void updateCount() override; + +private: + template + void update(); + QVector m_geometries; +}; + +} // namespace + +#endif diff --git a/x11eventfilter.cpp b/x11eventfilter.cpp index 4f7839b94a..f88711fbd9 100644 --- a/x11eventfilter.cpp +++ b/x11eventfilter.cpp @@ -19,7 +19,7 @@ along with this program. If not, see . *********************************************************************/ #include "x11eventfilter.h" -#include "workspace.h" +#include namespace KWin { diff --git a/xcbutils.h b/xcbutils.h index a77e2d89d7..9a03134cf6 100644 --- a/xcbutils.h +++ b/xcbutils.h @@ -861,6 +861,38 @@ public: } }; +XCB_WRAPPER_DATA(CrtcInfoData, xcb_randr_get_crtc_info, xcb_randr_crtc_t, xcb_timestamp_t) +class CrtcInfo : public Wrapper +{ +public: + CrtcInfo() = default; + CrtcInfo(const CrtcInfo&) = default; + explicit CrtcInfo(xcb_randr_crtc_t c, xcb_timestamp_t t) : Wrapper(c, t) {} + + inline QRect rect() { + const CrtcInfoData::reply_type *info = data(); + if (!info || info->num_outputs == 0 || info->mode == XCB_NONE || info->status != XCB_RANDR_SET_CONFIG_SUCCESS) { + return QRect(); + } + return QRect(info->x, info->y, info->width, info->height); + } +}; + +XCB_WRAPPER_DATA(CurrentResourcesData, xcb_randr_get_screen_resources_current, xcb_window_t) +class CurrentResources : public Wrapper +{ +public: + explicit CurrentResources(WindowId window) : Wrapper(window) {} + + inline xcb_randr_crtc_t *crtcs() { + if (isNull()) { + return nullptr; + } + return xcb_randr_get_screen_resources_current_crtcs(data()); + } +}; + +XCB_WRAPPER(SetCrtcConfig, xcb_randr_set_crtc_config, xcb_randr_crtc_t, xcb_timestamp_t, xcb_timestamp_t, int16_t, int16_t, xcb_randr_mode_t, uint16_t, uint32_t, const xcb_randr_output_t*) } class ExtensionData