From 1fa87b7c5a99c9c979db4aa541bde6f12abc86fb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Feb 2024 04:37:52 -0800 Subject: [PATCH] feat(slock): implement ext_session_lock_v1 backend note: did not run lints or fully test yet --- src/wayland/CMakeLists.txt | 3 +- src/wayland/session_lock/CMakeLists.txt | 12 ++ src/wayland/session_lock/lock.cpp | 42 +++++++ src/wayland/session_lock/lock.hpp | 38 ++++++ src/wayland/session_lock/manager.cpp | 22 ++++ src/wayland/session_lock/manager.hpp | 27 +++++ src/wayland/session_lock/session_lock.cpp | 107 +++++++++++++++++ src/wayland/session_lock/session_lock.hpp | 92 +++++++++++++++ .../session_lock/shell_integration.cpp | 20 ++++ .../session_lock/shell_integration.hpp | 13 ++ src/wayland/session_lock/surface.cpp | 111 ++++++++++++++++++ src/wayland/session_lock/surface.hpp | 39 ++++++ 12 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 src/wayland/session_lock/CMakeLists.txt create mode 100644 src/wayland/session_lock/lock.cpp create mode 100644 src/wayland/session_lock/lock.hpp create mode 100644 src/wayland/session_lock/manager.cpp create mode 100644 src/wayland/session_lock/manager.hpp create mode 100644 src/wayland/session_lock/session_lock.cpp create mode 100644 src/wayland/session_lock/session_lock.hpp create mode 100644 src/wayland/session_lock/shell_integration.cpp create mode 100644 src/wayland/session_lock/shell_integration.hpp create mode 100644 src/wayland/session_lock/surface.cpp create mode 100644 src/wayland/session_lock/surface.hpp diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 7f93c5e..5588979 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -22,7 +22,7 @@ message(STATUS "Found wayland-scanner at ${waylandscanner}") execute_process( COMMAND pkg-config --variable=pkgdatadir wayland-protocols - OUTPUT_VARIABLE WAYLAND_PROTOCOLS_DIR + OUTPUT_VARIABLE WAYLAND_PROTOCOLS OUTPUT_STRIP_TRAILING_WHITESPACE ) @@ -54,6 +54,7 @@ qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland) add_library(quickshell-wayland-init OBJECT init.cpp) add_subdirectory(wlr_layershell) +add_subdirectory(session_lock) target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt new file mode 100644 index 0000000..23d9039 --- /dev/null +++ b/src/wayland/session_lock/CMakeLists.txt @@ -0,0 +1,12 @@ +qt_add_library(quickshell-wayland-sessionlock STATIC + manager.cpp + surface.cpp + lock.cpp + shell_integration.cpp + session_lock.cpp +) + +wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml") +target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client) + +target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-sessionlock) diff --git a/src/wayland/session_lock/lock.cpp b/src/wayland/session_lock/lock.cpp new file mode 100644 index 0000000..be64a4c --- /dev/null +++ b/src/wayland/session_lock/lock.cpp @@ -0,0 +1,42 @@ +#include "lock.hpp" + +#include +#include + +#include "manager.hpp" + +QSWaylandSessionLock::QSWaylandSessionLock( + QSWaylandSessionLockManager* manager, + ::ext_session_lock_v1* lock +) + : manager(manager) { + this->init(lock); // if isInitialized is false that means we already unlocked. +} + +QSWaylandSessionLock::~QSWaylandSessionLock() { this->unlock(); } + +void QSWaylandSessionLock::unlock() { + if (this->isInitialized()) { + if (this->locked) this->unlock_and_destroy(); + else this->destroy(); + + this->locked = false; + this->manager->active = nullptr; + + emit this->unlocked(); + } +} + +bool QSWaylandSessionLock::active() const { return this->isInitialized(); } + +bool QSWaylandSessionLock::hasCompositorLock() const { return this->locked; } + +void QSWaylandSessionLock::ext_session_lock_v1_locked() { + this->locked = true; + emit this->compositorLocked(); +} + +void QSWaylandSessionLock::ext_session_lock_v1_finished() { + this->locked = false; + this->unlock(); +} diff --git a/src/wayland/session_lock/lock.hpp b/src/wayland/session_lock/lock.hpp new file mode 100644 index 0000000..d28a735 --- /dev/null +++ b/src/wayland/session_lock/lock.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +class QSWaylandSessionLockManager; + +class QSWaylandSessionLock + : public QObject + , public QtWayland::ext_session_lock_v1 { + Q_OBJECT; + +public: + QSWaylandSessionLock(QSWaylandSessionLockManager* manager, ::ext_session_lock_v1* lock); + ~QSWaylandSessionLock() override; + Q_DISABLE_COPY_MOVE(QSWaylandSessionLock); + + void unlock(); + + // Returns true if the lock has not finished. + [[nodiscard]] bool active() const; + // Returns true if the compositor considers the session to be locked. + [[nodiscard]] bool hasCompositorLock() const; + +signals: + void compositorLocked(); + void unlocked(); + +private: + void ext_session_lock_v1_locked() override; + void ext_session_lock_v1_finished() override; + + QSWaylandSessionLockManager* manager; // static and not dealloc'd + + // true when the compositor determines the session is locked + bool locked = false; +}; diff --git a/src/wayland/session_lock/manager.cpp b/src/wayland/session_lock/manager.cpp new file mode 100644 index 0000000..60c24f6 --- /dev/null +++ b/src/wayland/session_lock/manager.cpp @@ -0,0 +1,22 @@ +#include "manager.hpp" + +#include +#include +#include + +#include "lock.hpp" + +QSWaylandSessionLockManager::QSWaylandSessionLockManager() + : QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +QSWaylandSessionLockManager::~QSWaylandSessionLockManager() { this->destroy(); } + +QSWaylandSessionLock* QSWaylandSessionLockManager::acquireLock() { + if (this->isLocked()) return nullptr; + this->active = new QSWaylandSessionLock(this, this->lock()); + return this->active; +} + +bool QSWaylandSessionLockManager::isLocked() const { return this->active != nullptr; } diff --git a/src/wayland/session_lock/manager.hpp b/src/wayland/session_lock/manager.hpp new file mode 100644 index 0000000..8c36aa5 --- /dev/null +++ b/src/wayland/session_lock/manager.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include "lock.hpp" + +class QSWaylandSessionLockManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_session_lock_manager_v1 { +public: + QSWaylandSessionLockManager(); + ~QSWaylandSessionLockManager() override; + Q_DISABLE_COPY_MOVE(QSWaylandSessionLockManager); + + // Create a new session lock if there is no currently active lock, otherwise null. + QSWaylandSessionLock* acquireLock(); + [[nodiscard]] bool isLocked() const; + + static bool sessionLocked(); + +private: + QSWaylandSessionLock* active = nullptr; + + friend class QSWaylandSessionLock; +}; diff --git a/src/wayland/session_lock/session_lock.cpp b/src/wayland/session_lock/session_lock.cpp new file mode 100644 index 0000000..3ab4222 --- /dev/null +++ b/src/wayland/session_lock/session_lock.cpp @@ -0,0 +1,107 @@ +#include "session_lock.hpp" + +#include +#include +#include +#include + +#include "lock.hpp" +#include "manager.hpp" +#include "shell_integration.hpp" +#include "surface.hpp" + +static QSWaylandSessionLockManager* manager() { + static QSWaylandSessionLockManager* manager = nullptr; + + if (manager == nullptr) { + manager = new QSWaylandSessionLockManager(); + } + + return manager; +} + +bool SessionLockManager::lock() { + if (SessionLockManager::sessionLocked()) return false; + this->mLock = manager()->acquireLock(); + this->mLock->setParent(this); + + // clang-format off + QObject::connect(this->mLock, &QSWaylandSessionLock::compositorLocked, this, &SessionLockManager::locked); + QObject::connect(this->mLock, &QSWaylandSessionLock::unlocked, this, &SessionLockManager::unlocked); + // clang-format on + return true; +} + +bool SessionLockManager::unlock() { + if (!this->isLocked()) return false; + auto* lock = this->mLock; + this->mLock = nullptr; + delete lock; + return true; +} + +bool SessionLockManager::isLocked() const { return this->mLock != nullptr; } +bool SessionLockManager::sessionLocked() { return manager()->isLocked(); } + +LockWindowExtension::~LockWindowExtension() { + if (this->surface != nullptr) { + this->surface->setExtension(nullptr); + } +} + +LockWindowExtension* LockWindowExtension::get(QWindow* window) { + auto v = window->property("sessionlock_ext"); + + if (v.canConvert()) { + return v.value(); + } else { + return nullptr; + } +} + +bool LockWindowExtension::attach(QWindow* window, SessionLockManager* manager) { + if (this->surface != nullptr) throw "Cannot change the attached window of a LockWindowExtension"; + + auto* current = LockWindowExtension::get(window); + QtWaylandClient::QWaylandWindow* waylandWindow = nullptr; + + if (current != nullptr) { + current->surface->setExtension(this); + } else { + window->create(); + + waylandWindow = dynamic_cast(window->handle()); + if (waylandWindow == nullptr) { + qWarning() << window << "is not a wayland window. Cannot create lock surface."; + return false; + } + + static QSWaylandSessionLockIntegration* lockIntegration = nullptr; + if (lockIntegration == nullptr) { + lockIntegration = new QSWaylandSessionLockIntegration(); + if (!lockIntegration->initialize(waylandWindow->display())) { + delete lockIntegration; + lockIntegration = nullptr; + qWarning() << "Failed to initialize lockscreen integration"; + } + } + + waylandWindow->setShellIntegration(lockIntegration); + } + + this->setParent(window); + window->setProperty("sessionlock_ext", QVariant::fromValue(this)); + this->lock = manager->mLock; + + if (waylandWindow != nullptr) { + this->surface = new QSWaylandSessionLockSurface(waylandWindow); + if (this->immediatelyVisible) this->surface->setVisible(); + } + + return true; +} + +void LockWindowExtension::setVisible() { + if (this->surface == nullptr) immediatelyVisible = true; + else this->surface->setVisible(); +} diff --git a/src/wayland/session_lock/session_lock.hpp b/src/wayland/session_lock/session_lock.hpp new file mode 100644 index 0000000..78581ab --- /dev/null +++ b/src/wayland/session_lock/session_lock.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include + +class QSWaylandSessionLock; +class QSWaylandSessionLockSurface; +class QSWaylandSessionLockIntegration; + +class SessionLockManager: public QObject { + Q_OBJECT; + +public: + SessionLockManager(QObject* parent = nullptr): QObject(parent) {} + Q_DISABLE_COPY_MOVE(SessionLockManager); + + // Returns true if a lock was acquired. + // If true is returned the caller must watch the global screen list and create/destroy + // windows with an attached LockWindowExtension to match it. + bool lock(); + + // Returns true if the session was locked and is now unlocked. + bool unlock(); + + [[nodiscard]] bool isLocked() const; + + static bool sessionLocked(); + +signals: + // This signal is sent once the compositor considers the session to be fully locked. + // This corrosponds to the ext_session_lock_v1::locked event. + void locked(); + + // This signal is sent once the compositor considers the session to be unlocked. + // This corrosponds to the ext_session_lock_v1::finished event. + // + // The session lock will end in one of three cases. + // 1. unlock() is called. + // 2. The SessionLockManager is destroyed. + // 3. The compositor forcibly unlocks the session. + // + // After receiving this event the caller should destroy all of its lock surfaces. + void unlocked(); + +private slots: + //void onUnlocked(); + +private: + QSWaylandSessionLock* mLock = nullptr; + + friend class LockWindowExtension; +}; + +class LockWindowExtension: public QObject { + Q_OBJECT; + +public: + LockWindowExtension(QObject* parent = nullptr): QObject(parent) {} + ~LockWindowExtension() override; + + // Attach this lock extension to the given window. + // The extension is reparented to the window and replaces any existing lock extension. + // Returns false if the window cannot be used. + bool attach(QWindow* window, SessionLockManager* manager); + + // This must be called in place of QWindow::setVisible. Calling QWindow::setVisible will result in a crash. + // To make a window invisible, destroy it as it cannot be recovered. + void setVisible(); + + [[nodiscard]] bool isLocked() const; + + static LockWindowExtension* get(QWindow* window); + +signals: + // This signal is sent once the compositor considers the session to be fully locked. + // See SessionLockManager::locked for details. + void locked(); + + // After receiving this signal the window is no longer in use by the compositor + // and should be destroyed. See SessionLockManager::unlocked for details. + void unlocked(); + +private: + QSWaylandSessionLockSurface* surface = nullptr; + QSWaylandSessionLock* lock = nullptr; + bool immediatelyVisible = false; + + friend class QSWaylandSessionLockSurface; + friend class QSWaylandSessionLockIntegration; +}; diff --git a/src/wayland/session_lock/shell_integration.cpp b/src/wayland/session_lock/shell_integration.cpp new file mode 100644 index 0000000..da0e1f1 --- /dev/null +++ b/src/wayland/session_lock/shell_integration.cpp @@ -0,0 +1,20 @@ +#include "shell_integration.hpp" + +#include +#include +#include + +#include "session_lock.hpp" +#include "surface.hpp" + +QtWaylandClient::QWaylandShellSurface* +QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) { + auto* lock = LockWindowExtension::get(window->window()); + if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) { + qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to " + "visible via LockWindowExtension::setVisible"; + throw nullptr; + } + + return lock->surface; +} diff --git a/src/wayland/session_lock/shell_integration.hpp b/src/wayland/session_lock/shell_integration.hpp new file mode 100644 index 0000000..d6f9175 --- /dev/null +++ b/src/wayland/session_lock/shell_integration.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include +#include + +class QSWaylandSessionLockIntegration: public QtWaylandClient::QWaylandShellIntegration { +public: + bool initialize(QtWaylandClient::QWaylandDisplay* /* display */) override { return true; } + QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window + ) override; +}; diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp new file mode 100644 index 0000000..ed409db --- /dev/null +++ b/src/wayland/session_lock/surface.cpp @@ -0,0 +1,111 @@ +#include "surface.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lock.hpp" +#include "session_lock.hpp" + +QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWaylandWindow* window) + : QtWaylandClient::QWaylandShellSurface(window) { + auto* qwindow = window->window(); + this->setExtension(LockWindowExtension::get(qwindow)); + + if (this->ext == nullptr) { + qFatal() << "QSWaylandSessionLockSurface created with null LockWindowExtension"; + throw nullptr; + } + + if (this->ext->lock == nullptr) { + qFatal() << "QSWaylandSessionLock for QSWaylandSessionLockSurface died"; + throw nullptr; + } + + wl_output* output = nullptr; + auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); + + if (waylandScreen != nullptr) { + output = waylandScreen->output(); + } else { + qFatal() << "Session lock screen does not corrospond to a real screen. Force closing window"; + throw nullptr; + } + + this->init(this->ext->lock->get_lock_surface(window->waylandSurface()->object(), output)); +} + +QSWaylandSessionLockSurface::~QSWaylandSessionLockSurface() { this->destroy(); } + +bool QSWaylandSessionLockSurface::isExposed() const { return this->configured; } + +void QSWaylandSessionLockSurface::applyConfigure() { + this->window()->resizeFromApplyConfigure(this->size); +} + +bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { + if (this->initBuf != nullptr) { + // at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one. + delete this->initBuf; + this->initBuf = nullptr; + } + + return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region); +} + +void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { + if (ext == nullptr) { + this->window()->window()->close(); + } else { + if (this->ext != nullptr) { + this->ext->surface = nullptr; + } + + this->ext = ext; + this->ext->surface = this; + } +} + +void QSWaylandSessionLockSurface::setVisible() { + if (this->configured && !this->visible) initVisible(); + this->visible = true; +} + +void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( + quint32 serial, + quint32 width, + quint32 height +) { + this->ack_configure(serial); + + this->size = QSize(static_cast(width), static_cast(height)); + if (!this->configured) { + this->configured = true; + + this->window()->resizeFromApplyConfigure(this->size); + this->window()->handleExpose(QRect(QPoint(), this->size)); + if (this->visible) initVisible(); + } else { + this->window()->applyConfigureWhenPossible(); + } +} + +void QSWaylandSessionLockSurface::initVisible() { + this->visible = true; + + // qt always commits a null buffer in QWaylandWindow::initWindow. + // We attach a dummy buffer to satisfy ext_session_lock_v1. + this->initBuf = new QtWaylandClient::QWaylandShmBuffer( + this->window()->display(), + size, + QImage::Format_ARGB32 + ); + + this->window()->waylandSurface()->attach(initBuf->buffer(), 0, 0); + this->window()->window()->setVisible(true); +} diff --git a/src/wayland/session_lock/surface.hpp b/src/wayland/session_lock/surface.hpp new file mode 100644 index 0000000..fd5e0a1 --- /dev/null +++ b/src/wayland/session_lock/surface.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "session_lock.hpp" + +class QSWaylandSessionLockSurface + : public QtWaylandClient::QWaylandShellSurface + , public QtWayland::ext_session_lock_surface_v1 { +public: + QSWaylandSessionLockSurface(QtWaylandClient::QWaylandWindow* window); + ~QSWaylandSessionLockSurface() override; + Q_DISABLE_COPY_MOVE(QSWaylandSessionLockSurface); + + [[nodiscard]] bool isExposed() const override; + void applyConfigure() override; + bool handleExpose(const QRegion& region) override; + + void setExtension(LockWindowExtension*); + void setVisible(); + +private: + void + ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; + + void initVisible(); + + LockWindowExtension* ext = nullptr; + QSize size; + bool configured = false; + bool visible = false; + QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr; +};