diff --git a/CMakeLists.txt b/CMakeLists.txt index 3161fe0450..c58268d271 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -430,6 +430,7 @@ if(Wayland_Client_FOUND AND XKB_FOUND) wayland_client/registry.cpp wayland_client/fullscreen_shell.cpp wayland_client/output.cpp + wayland_client/shell.cpp ${CMAKE_BINARY_DIR}/wayland_protocols/wayland-client-fullscreen-shell.c ) if(KWIN_HAVE_EGL AND Wayland_Egl_FOUND) diff --git a/autotests/wayland_client/CMakeLists.txt b/autotests/wayland_client/CMakeLists.txt index 25e4751b35..7271a6bf68 100644 --- a/autotests/wayland_client/CMakeLists.txt +++ b/autotests/wayland_client/CMakeLists.txt @@ -56,3 +56,20 @@ add_dependencies(testWaylandOutput wayland-client-fullscreen-shell) target_link_libraries( testWaylandOutput Qt5::Test Wayland::Client) add_test(kwin-testWaylandOutput testWaylandOutput) ecm_mark_as_test(testWaylandOutput) + +######################################################## +# Test WaylandShell +######################################################## +set( testWaylandShell_SRCS + test_wayland_shell.cpp + ${KWIN_SOURCE_DIR}/wayland_client/connection_thread.cpp + ${KWIN_SOURCE_DIR}/wayland_client/registry.cpp + ${KWIN_SOURCE_DIR}/wayland_client/fullscreen_shell.cpp + ${KWIN_SOURCE_DIR}/wayland_client/shell.cpp + ${CMAKE_BINARY_DIR}/wayland_protocols/wayland-client-fullscreen-shell.c + ) +add_executable(testWaylandShell ${testWaylandShell_SRCS}) +add_dependencies(testWaylandShell wayland-client-fullscreen-shell) +target_link_libraries( testWaylandShell Qt5::Test Wayland::Client) +add_test(kwin-testWaylandShell testWaylandShell) +ecm_mark_as_test(testWaylandShell) diff --git a/autotests/wayland_client/test_wayland_shell.cpp b/autotests/wayland_client/test_wayland_shell.cpp new file mode 100644 index 0000000000..5be894b7c6 --- /dev/null +++ b/autotests/wayland_client/test_wayland_shell.cpp @@ -0,0 +1,174 @@ +/******************************************************************** +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 . +*********************************************************************/ +// Qt +#include +// KWin +#include "../../wayland_client/connection_thread.h" +#include "../../wayland_client/shell.h" +#include "../../wayland_client/registry.h" +// Wayland +#include + +class TestWaylandShell : public QObject +{ + Q_OBJECT +public: + explicit TestWaylandShell(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testRegistry(); + void testShell(); + + // TODO: add tests for removal - requires more control over the compositor + +private: + QProcess *m_westonProcess; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-wayland-shell-0"); + +TestWaylandShell::TestWaylandShell(QObject *parent) + : QObject(parent) + , m_westonProcess(nullptr) +{ +} + +void TestWaylandShell::init() +{ + QVERIFY(!m_westonProcess); + // starts weston + m_westonProcess = new QProcess(this); + m_westonProcess->setProgram(QStringLiteral("weston")); + + m_westonProcess->setArguments(QStringList({QStringLiteral("--socket=%1").arg(s_socketName), + QStringLiteral("--use-pixman"), + QStringLiteral("--width=1024"), + QStringLiteral("--height=768")})); + m_westonProcess->start(); + QVERIFY(m_westonProcess->waitForStarted()); + + // wait for the socket to appear + QDir runtimeDir(qgetenv("XDG_RUNTIME_DIR")); + if (runtimeDir.exists(s_socketName)) { + return; + } + QFileSystemWatcher *socketWatcher = new QFileSystemWatcher(QStringList({runtimeDir.absolutePath()}), this); + QSignalSpy socketSpy(socketWatcher, SIGNAL(directoryChanged(QString))); + + // limit to maximum of 10 waits + for (int i = 0; i < 10; ++i) { + QVERIFY(socketSpy.wait()); + if (runtimeDir.exists(s_socketName)) { + delete socketWatcher; + return; + } + } +} + +void TestWaylandShell::cleanup() +{ + // terminates weston + m_westonProcess->terminate(); + QVERIFY(m_westonProcess->waitForFinished()); + delete m_westonProcess; + m_westonProcess = nullptr; +} + +void TestWaylandShell::testRegistry() +{ + if (m_westonProcess->state() != QProcess::Running) { + QSKIP("This test requires a running wayland server"); + } + KWin::Wayland::ConnectionThread connection; + QSignalSpy connectedSpy(&connection, SIGNAL(connected())); + connection.setSocketName(s_socketName); + connection.initConnection(); + QVERIFY(connectedSpy.wait()); + + KWin::Wayland::Registry registry; + QSignalSpy announced(®istry, SIGNAL(shellAnnounced(quint32,quint32))); + registry.create(connection.display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(connection.display()); + QVERIFY(announced.wait()); + + KWin::Wayland::Shell shell; + QVERIFY(!shell.isValid()); + + shell.setup(registry.bindShell(announced.first().first().value(), announced.first().last().value())); + wl_display_flush(connection.display()); + QVERIFY(shell.isValid()); + + shell.release(); + QVERIFY(!shell.isValid()); +} + +void TestWaylandShell::testShell() +{ + if (m_westonProcess->state() != QProcess::Running) { + QSKIP("This test requires a running wayland server"); + } + KWin::Wayland::ConnectionThread connection; + QSignalSpy connectedSpy(&connection, SIGNAL(connected())); + connection.setSocketName(s_socketName); + connection.initConnection(); + QVERIFY(connectedSpy.wait()); + + KWin::Wayland::Registry registry; + QSignalSpy compositorSpy(®istry, SIGNAL(compositorAnnounced(quint32,quint32))); + QSignalSpy announced(®istry, SIGNAL(shellAnnounced(quint32,quint32))); + registry.create(connection.display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(connection.display()); + QVERIFY(announced.wait()); + + if (compositorSpy.isEmpty()) { + QVERIFY(compositorSpy.wait()); + } + wl_compositor *compositor = registry.bindCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value()); + + KWin::Wayland::Shell shell; + shell.setup(registry.bindShell(announced.first().first().value(), announced.first().last().value())); + wl_display_flush(connection.display()); + KWin::Wayland::ShellSurface *surface = shell.createSurface(wl_compositor_create_surface(compositor), &shell); + QSignalSpy sizeSpy(surface, SIGNAL(sizeChanged(QSize))); + QVERIFY(sizeSpy.isValid()); + QCOMPARE(surface->size(), QSize()); + + surface->setFullscreen(); + wl_display_flush(connection.display()); + QVERIFY(sizeSpy.wait()); + QCOMPARE(sizeSpy.count(), 1); + QCOMPARE(sizeSpy.first().first().toSize(), QSize(1024, 768)); + QCOMPARE(surface->size(), QSize(1024, 768)); + + QVERIFY(surface->isValid()); + shell.release(); + QVERIFY(!surface->isValid()); + + wl_compositor_destroy(compositor); +} + +QTEST_MAIN(TestWaylandShell) +#include "test_wayland_shell.moc" diff --git a/wayland_backend.cpp b/wayland_backend.cpp index cd94ab2c2a..e19545947d 100644 --- a/wayland_backend.cpp +++ b/wayland_backend.cpp @@ -26,6 +26,7 @@ along with this program. If not, see . #include "wayland_client/fullscreen_shell.h" #include "wayland_client/output.h" #include "wayland_client/registry.h" +#include "wayland_client/shell.h" // Qt #include #include @@ -46,36 +47,6 @@ namespace KWin namespace Wayland { -/** - * Call back for ping from Wayland Shell. - **/ -static void handlePing(void *data, struct wl_shell_surface *shellSurface, uint32_t serial) -{ - Q_UNUSED(shellSurface); - reinterpret_cast(data)->ping(serial); -} - -/** - * Callback for a configure request for a shell surface - **/ -static void handleConfigure(void *data, struct wl_shell_surface *shellSurface, uint32_t edges, int32_t width, int32_t height) -{ - Q_UNUSED(shellSurface) - Q_UNUSED(edges) - WaylandBackend *display = reinterpret_cast(data); - display->setShellSurfaceSize(QSize(width, height)); - // TODO: this information should probably go into Screens -} - -/** - * Callback for popups - not needed, we don't have popups - **/ -static void handlePopupDone(void *data, struct wl_shell_surface *shellSurface) -{ - Q_UNUSED(data) - Q_UNUSED(shellSurface) -} - static void seatHandleCapabilities(void *data, wl_seat *seat, uint32_t capabilities) { WaylandSeat *s = reinterpret_cast(data); @@ -186,12 +157,6 @@ static void bufferRelease(void *data, wl_buffer *wl_buffer) } // handlers -static const struct wl_shell_surface_listener s_shellSurfaceListener = { - handlePing, - handleConfigure, - handlePopupDone -}; - static const struct wl_pointer_listener s_pointerListener = { pointerHandleEnter, pointerHandleLeave, @@ -597,16 +562,16 @@ WaylandBackend::WaylandBackend(QObject *parent) , m_eventQueue(nullptr) , m_registry(new Registry(this)) , m_compositor(NULL) - , m_shell(NULL) + , m_shell(new Shell(this)) , m_surface(NULL) , m_shellSurface(NULL) - , m_shellSurfaceSize(displayWidth(), displayHeight()) , m_seat() , m_shm() , m_connectionThreadObject(nullptr) , m_connectionThread(nullptr) , m_fullscreenShell(new FullscreenShell(this)) { + connect(this, &WaylandBackend::shellSurfaceSizeChanged, this, &WaylandBackend::checkBackendReady); connect(m_registry, &Registry::compositorAnnounced, this, [this](quint32 name) { setCompositor(m_registry->bindCompositor(name, 1)); @@ -614,7 +579,8 @@ WaylandBackend::WaylandBackend(QObject *parent) ); connect(m_registry, &Registry::shellAnnounced, this, [this](quint32 name) { - setShell(m_registry->bindShell(name, 1)); + m_shell->setup(m_registry->bindShell(name, 1)); + createSurface(); } ); connect(m_registry, &Registry::outputAnnounced, this, @@ -637,15 +603,13 @@ WaylandBackend::~WaylandBackend() { destroyOutputs(); if (m_shellSurface) { - wl_shell_surface_destroy(m_shellSurface); + m_shellSurface->release(); } m_fullscreenShell->release(); if (m_surface) { wl_surface_destroy(m_surface); } - if (m_shell) { - wl_shell_destroy(m_shell); - } + m_shell->release(); if (m_compositor) { wl_compositor_destroy(m_compositor); } @@ -698,7 +662,8 @@ void WaylandBackend::initConnection() m_shm.reset(); destroyOutputs(); if (m_shellSurface) { - free(m_shellSurface); + m_shellSurface->destroy(); + delete m_shellSurface; m_shellSurface = nullptr; } m_fullscreenShell->destroy(); @@ -707,8 +672,7 @@ void WaylandBackend::initConnection() m_surface = nullptr; } if (m_shell) { - free(m_shell); - m_shell = nullptr; + m_shell->destroy(); } if (m_compositor) { free(m_compositor); @@ -751,24 +715,30 @@ void WaylandBackend::createSurface() } if (m_fullscreenShell->isValid()) { Output *o = m_outputs.first(); + m_fullscreenShell->present(m_surface, o->output()); if (o->pixelSize().isValid()) { - setShellSurfaceSize(o->pixelSize()); + emit shellSurfaceSizeChanged(o->pixelSize()); } connect(o, &Output::changed, this, [this, o]() { if (o->pixelSize().isValid()) { - setShellSurfaceSize(o->pixelSize()); + emit shellSurfaceSizeChanged(o->pixelSize()); } } ); - m_fullscreenShell->present(m_surface, o->output()); - } else { + } else if (m_shell->isValid()) { // map the surface as fullscreen - m_shellSurface = wl_shell_get_shell_surface(m_shell, m_surface); - wl_shell_surface_add_listener(m_shellSurface, &s_shellSurfaceListener, this); - wl_shell_surface_set_fullscreen(m_shellSurface, WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT, 0, NULL); + m_shellSurface = m_shell->createSurface(m_surface, this); + m_shellSurface->setFullscreen(); + connect(m_shellSurface, &ShellSurface::pinged, this, + [this]() { + if (!m_seat.isNull()) { + m_seat->resetCursor(); + } + } + ); + connect(m_shellSurface, &ShellSurface::sizeChanged, this, &WaylandBackend::shellSurfaceSizeChanged); } - emit backendReady(); } void WaylandBackend::createShm(uint32_t name) @@ -779,29 +749,6 @@ void WaylandBackend::createShm(uint32_t name) } } -void WaylandBackend::ping(uint32_t serial) -{ - wl_shell_surface_pong(m_shellSurface, serial); - if (!m_seat.isNull()) { - m_seat->resetCursor(); - } -} - -void WaylandBackend::setShell(wl_shell *s) -{ - m_shell = s; - createSurface(); -} - -void WaylandBackend::setShellSurfaceSize(const QSize &size) -{ - if (m_shellSurfaceSize == size) { - return; - } - m_shellSurfaceSize = size; - emit shellSurfaceSizeChanged(m_shellSurfaceSize); -} - void WaylandBackend::addOutput(wl_output *o) { Output *output = new Output(this); @@ -821,6 +768,26 @@ wl_registry *WaylandBackend::registry() return m_registry->registry(); } +QSize WaylandBackend::shellSurfaceSize() const +{ + if (m_shellSurface) { + return m_shellSurface->size(); + } + if (m_fullscreenShell->isValid()) { + return m_outputs.first()->pixelSize(); + } + return QSize(); +} + +void WaylandBackend::checkBackendReady() +{ + if (!shellSurfaceSize().isValid()) { + return; + } + disconnect(this, &WaylandBackend::shellSurfaceSizeChanged, this, &WaylandBackend::checkBackendReady); + emit backendReady(); +} + } } // KWin diff --git a/wayland_backend.h b/wayland_backend.h index 981c1821d1..f9e9837565 100644 --- a/wayland_backend.h +++ b/wayland_backend.h @@ -50,6 +50,8 @@ class ConnectionThread; class FullscreenShell; class Output; class Registry; +class Shell; +class ShellSurface; class CursorData { @@ -179,18 +181,14 @@ public: wl_registry *registry(); void setCompositor(wl_compositor *c); wl_compositor *compositor(); - void setShell(wl_shell *s); - wl_shell *shell(); void addOutput(wl_output *o); const QList &outputs() const; ShmPool *shmPool(); void createSeat(uint32_t name); void createShm(uint32_t name); - void ping(uint32_t serial); wl_surface *surface() const; - const QSize &shellSurfaceSize() const; - void setShellSurfaceSize(const QSize &size); + QSize shellSurfaceSize() const; void installCursorImage(Qt::CursorShape shape); void dispatchEvents(); @@ -204,14 +202,14 @@ private: void initConnection(); void createSurface(); void destroyOutputs(); + void checkBackendReady(); wl_display *m_display; wl_event_queue *m_eventQueue; Registry *m_registry; wl_compositor *m_compositor; - wl_shell *m_shell; + Shell *m_shell; wl_surface *m_surface; - wl_shell_surface *m_shellSurface; - QSize m_shellSurfaceSize; + ShellSurface *m_shellSurface; QScopedPointer m_seat; QScopedPointer m_shm; QList m_outputs; @@ -282,12 +280,6 @@ wl_compositor *WaylandBackend::compositor() return m_compositor; } -inline -wl_shell *WaylandBackend::shell() -{ - return m_shell; -} - inline ShmPool* WaylandBackend::shmPool() { @@ -300,12 +292,6 @@ wl_surface *WaylandBackend::surface() const return m_surface; } -inline -const QSize &WaylandBackend::shellSurfaceSize() const -{ - return m_shellSurfaceSize; -} - inline const QList< Output* >& WaylandBackend::outputs() const { diff --git a/wayland_client/shell.cpp b/wayland_client/shell.cpp new file mode 100644 index 0000000000..ffeb47f590 --- /dev/null +++ b/wayland_client/shell.cpp @@ -0,0 +1,164 @@ +/******************************************************************** + 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 "shell.h" +#include "output.h" + +namespace KWin +{ +namespace Wayland +{ + +Shell::Shell(QObject *parent) + : QObject(parent) + , m_shell(nullptr) +{ +} + +Shell::~Shell() +{ + release(); +} + +void Shell::destroy() +{ + if (!m_shell) { + return; + } + emit interfaceAboutToBeDestroyed(); + free(m_shell); + m_shell = nullptr; +} + +void Shell::release() +{ + if (!m_shell) { + return; + } + emit interfaceAboutToBeReleased(); + wl_shell_destroy(m_shell); + m_shell = nullptr; +} + +void Shell::setup(wl_shell *shell) +{ + Q_ASSERT(!m_shell); + Q_ASSERT(shell); + m_shell = shell; +} + +ShellSurface *Shell::createSurface(wl_surface *surface, QObject *parent) +{ + Q_ASSERT(isValid()); + ShellSurface *s = new ShellSurface(parent); + connect(this, &Shell::interfaceAboutToBeReleased, s, &ShellSurface::release); + connect(this, &Shell::interfaceAboutToBeDestroyed, s, &ShellSurface::destroy); + s->setup(wl_shell_get_shell_surface(m_shell, surface)); + return s; +} + +ShellSurface::ShellSurface(QObject *parent) + : QObject(parent) + , m_surface(nullptr) + , m_size() +{ +} + +ShellSurface::~ShellSurface() +{ + release(); +} + +void ShellSurface::release() +{ + if (!isValid()) { + return; + } + wl_shell_surface_destroy(m_surface); + m_surface = nullptr; +} + +void ShellSurface::destroy() +{ + if (!isValid()) { + return; + } + free(m_surface); + m_surface = nullptr; +} + +const struct wl_shell_surface_listener ShellSurface::s_listener = { + ShellSurface::pingCallback, + ShellSurface::configureCallback, + ShellSurface::popupDoneCallback +}; + +void ShellSurface::configureCallback(void *data, wl_shell_surface *shellSurface, uint32_t edges, int32_t width, int32_t height) +{ + Q_UNUSED(edges) + ShellSurface *s = reinterpret_cast(data); + Q_ASSERT(s->m_surface == shellSurface); + s->setSize(QSize(width, height)); +} + +void ShellSurface::pingCallback(void *data, wl_shell_surface *shellSurface, uint32_t serial) +{ + ShellSurface *s = reinterpret_cast(data); + Q_ASSERT(s->m_surface == shellSurface); + s->ping(serial); +} + +void ShellSurface::popupDoneCallback(void *data, wl_shell_surface *shellSurface) +{ + // not needed, we don't have popups + Q_UNUSED(data) + Q_UNUSED(shellSurface) +} + +void ShellSurface::setup(wl_shell_surface *surface) +{ + Q_ASSERT(surface); + Q_ASSERT(!m_surface); + m_surface = surface; + wl_shell_surface_add_listener(m_surface, &s_listener, this); +} + +void ShellSurface::ping(uint32_t serial) +{ + wl_shell_surface_pong(m_surface, serial); + emit pinged(); +} + +void ShellSurface::setSize(const QSize &size) +{ + if (m_size == size) { + return; + } + m_size = size; + emit sizeChanged(size); +} + +void ShellSurface::setFullscreen(Output *output) +{ + Q_ASSERT(isValid()); + wl_shell_surface_set_fullscreen(m_surface, WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT, 0, output ? output->output() : nullptr); +} + +} +} diff --git a/wayland_client/shell.h b/wayland_client/shell.h new file mode 100644 index 0000000000..a61c081301 --- /dev/null +++ b/wayland_client/shell.h @@ -0,0 +1,112 @@ +/******************************************************************** + 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_WAYLAND_SHELL_H +#define KWIN_WAYLAND_SHELL_H + +#include +#include + +#include + +namespace KWin +{ +namespace Wayland +{ +class ShellSurface; +class Output; + +class Shell : public QObject +{ + Q_OBJECT +public: + explicit Shell(QObject *parent = nullptr); + virtual ~Shell(); + + bool isValid() const { + return m_shell != nullptr; + } + void release(); + void destroy(); + void setup(wl_shell *shell); + + ShellSurface *createSurface(wl_surface *surface, QObject *parent = nullptr); + + operator wl_shell*() { + return m_shell; + } + operator wl_shell*() const { + return m_shell; + } + +Q_SIGNALS: + void interfaceAboutToBeReleased(); + void interfaceAboutToBeDestroyed(); + +private: + wl_shell *m_shell; +}; + +class ShellSurface : public QObject +{ + Q_OBJECT + Q_PROPERTY(QSize size READ size WRITE setSize NOTIFY sizeChanged) +public: + explicit ShellSurface(QObject *parent); + virtual ~ShellSurface(); + + void release(); + void destroy(); + void setup(wl_shell_surface *surface); + QSize size() const { + return m_size; + } + void setSize(const QSize &size); + + void setFullscreen(Output *output = nullptr); + + bool isValid() const { + return m_surface != nullptr; + } + operator wl_shell_surface*() { + return m_surface; + } + operator wl_shell_surface*() const { + return m_surface; + } + + static void pingCallback(void *data, struct wl_shell_surface *shellSurface, uint32_t serial); + static void configureCallback(void *data, struct wl_shell_surface *shellSurface, uint32_t edges, int32_t width, int32_t height); + static void popupDoneCallback(void *data, struct wl_shell_surface *shellSurface); + +Q_SIGNALS: + void pinged(); + void sizeChanged(const QSize &); + +private: + void ping(uint32_t serial); + wl_shell_surface *m_surface; + QSize m_size; + static const struct wl_shell_surface_listener s_listener; +}; + +} +} + +#endif