diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index f3c7e1a..f95b6c3 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -134,8 +134,10 @@ void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); } - if (this->window == nullptr) this->mScreen = qscreen; - else this->window->setScreen(qscreen); + if (this->window == nullptr) { + this->mScreen = qscreen; + emit this->screenChanged(); + } else this->window->setScreen(qscreen); } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 5588979..60d9adb 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -46,6 +46,7 @@ endfunction() qt_add_library(quickshell-wayland STATIC wlr_layershell.cpp + session_lock.cpp ) qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland) diff --git a/src/wayland/module.md b/src/wayland/module.md index 4519413..7a427df 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -3,5 +3,6 @@ description = "Wayland specific Quickshell types" headers = [ "wlr_layershell/window.hpp", "wlr_layershell.hpp", + "session_lock.hpp", ] ----- diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp new file mode 100644 index 0000000..b11e414 --- /dev/null +++ b/src/wayland/session_lock.cpp @@ -0,0 +1,284 @@ +#include "session_lock.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "session_lock/session_lock.hpp" + +void SessionLock::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + + if (old != nullptr) { + QObject::disconnect(old->manager, nullptr, old, nullptr); + this->manager = old->manager; + this->manager->setParent(this); + } else { + this->manager = new SessionLockManager(this); + } + + // clang-format off + QObject::connect(this->manager, &SessionLockManager::locked, this, &SessionLock::secureStateChanged); + QObject::connect(this->manager, &SessionLockManager::unlocked, this, &SessionLock::secureStateChanged); + + QObject::connect(this->manager, &SessionLockManager::unlocked, this, &SessionLock::unlock); + + auto* app = QCoreApplication::instance(); + auto* guiApp = qobject_cast(app); + + if (guiApp != nullptr) { + QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &SessionLock::onScreensChanged); + QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &SessionLock::onScreensChanged); + QObject::connect(guiApp, &QGuiApplication::screenRemoved, this, &SessionLock::onScreensChanged); + } + // clang-format on + + if (this->lockTarget) { + this->manager->lock(); + this->updateSurfaces(old); + } else { + this->setLocked(false); + } +} + +void SessionLock::updateSurfaces(SessionLock* old) { + if (this->manager->isLocked()) { + auto screens = QGuiApplication::screens(); + + auto map = this->surfaces.toStdMap(); + for (auto& [screen, surface]: map) { + if (!screens.contains(screen)) { + this->surfaces.remove(screen); + surface->deleteLater(); + } + } + + if (this->mSurfaceComponent == nullptr) { + qWarning() << "SessionLock.surface is null. Aborting lock."; + this->unlock(); + return; + } + + for (auto* screen: screens) { + if (!this->surfaces.contains(screen)) { + auto* instanceObj = this->mSurfaceComponent->create(QQmlEngine::contextForObject(this)); + auto* instance = qobject_cast(instanceObj); + + if (instance == nullptr) { + qWarning() << "SessionLock.surface does not create a SessionLockSurface. Aborting lock."; + this->unlock(); + return; + } + + instance->setParent(this); + instance->setScreen(screen); + + auto* oldInstance = old == nullptr ? nullptr : old->surfaces.value(screen, nullptr); + instance->onReload(oldInstance); + + this->surfaces[screen] = instance; + instance->show(); + } + } + } +} + +void SessionLock::unlock() { + if (this->isLocked()) { + this->lockTarget = false; + this->manager->unlock(); + + for (auto* surface: this->surfaces) { + surface->deleteLater(); + } + + this->surfaces.clear(); + + emit this->lockStateChanged(); + } +} + +void SessionLock::onScreensChanged() { this->updateSurfaces(); } + +bool SessionLock::isLocked() const { + return this->manager == nullptr ? this->lockTarget : this->manager->isLocked(); +} + +bool SessionLock::isSecure() const { + return this->manager != nullptr && SessionLockManager::sessionLocked(); +} + +void SessionLock::setLocked(bool locked) { + if (this->isLocked() == locked) return; + this->lockTarget = locked; + + if (this->manager == nullptr) { + emit this->lockStateChanged(); + return; + } + + if (locked) { + this->manager->lock(); + this->updateSurfaces(); + if (this->lockTarget) emit this->lockStateChanged(); + } else { + this->unlock(); // emits lockStateChanged + } +} + +QQmlComponent* SessionLock::surfaceComponent() const { return this->mSurfaceComponent; } + +void SessionLock::rip() { + if (this->isLocked()) { + exit(1); + } +} + +void SessionLock::setSurfaceComponent(QQmlComponent* surfaceComponent) { + if (this->mSurfaceComponent != nullptr) this->mSurfaceComponent->deleteLater(); + if (surfaceComponent != nullptr) surfaceComponent->setParent(this); + + this->mSurfaceComponent = surfaceComponent; + emit this->surfaceComponentChanged(); +} + +SessionLockSurface::SessionLockSurface(QObject* parent) + : Reloadable(parent) + , mContentItem(new QQuickItem()) + , ext(new LockWindowExtension(this)) { + this->mContentItem->setParent(this); + + // clang-format off + QObject::connect(this, &SessionLockSurface::widthChanged, this, &SessionLockSurface::onWidthChanged); + QObject::connect(this, &SessionLockSurface::heightChanged, this, &SessionLockSurface::onHeightChanged); + // clang-format on +} + +SessionLockSurface::~SessionLockSurface() { + if (this->window != nullptr) { + this->window->deleteLater(); + } +} + +void SessionLockSurface::onReload(QObject* oldInstance) { + if (auto* old = qobject_cast(oldInstance)) { + this->window = old->disownWindow(); + } + + if (this->window == nullptr) { + this->window = new QQuickWindow(); + } + + this->mContentItem->setParentItem(this->window->contentItem()); + + this->mContentItem->setWidth(this->width()); + this->mContentItem->setHeight(this->height()); + + if (this->mScreen != nullptr) this->window->setScreen(this->mScreen); + this->window->setColor(this->mColor); + + // clang-format off + QObject::connect(this->window, &QWindow::visibilityChanged, this, &SessionLockSurface::visibleChanged); + QObject::connect(this->window, &QWindow::widthChanged, this, &SessionLockSurface::widthChanged); + QObject::connect(this->window, &QWindow::heightChanged, this, &SessionLockSurface::heightChanged); + QObject::connect(this->window, &QWindow::screenChanged, this, &SessionLockSurface::screenChanged); + QObject::connect(this->window, &QQuickWindow::colorChanged, this, &SessionLockSurface::colorChanged); + // clang-format on + + if (auto* parent = qobject_cast(this->parent())) { + if (!this->ext->attach(window, parent->manager)) { + qWarning( + ) << "Failed to attach LockWindowExtension to window. Surface will not behave correctly."; + } + } else { + qWarning( + ) << "SessionLockSurface parent is not a SessionLock. Surface will not behave correctly."; + } + + // without this the dangling screen pointer wont be updated to a real screen + emit this->screenChanged(); +} + +QQuickWindow* SessionLockSurface::disownWindow() { + QObject::disconnect(this->window, nullptr, this, nullptr); + this->mContentItem->setParentItem(nullptr); + + auto* window = this->window; + this->window = nullptr; + return window; +} + +void SessionLockSurface::show() { this->ext->setVisible(); } + +QQuickItem* SessionLockSurface::contentItem() const { return this->mContentItem; } + +bool SessionLockSurface::isVisible() const { return this->window->isVisible(); } + +qint32 SessionLockSurface::width() const { + if (this->window == nullptr) return 0; + else return this->window->width(); +} + +qint32 SessionLockSurface::height() const { + if (this->window == nullptr) return 0; + else return this->window->height(); +} + +QuickshellScreenInfo* SessionLockSurface::screen() const { + QScreen* qscreen = nullptr; + + if (this->window == nullptr) { + if (this->mScreen != nullptr) qscreen = this->mScreen; + } else { + qscreen = this->window->screen(); + } + + return new QuickshellScreenInfo( + const_cast(this), // NOLINT + qscreen + ); +} + +void SessionLockSurface::setScreen(QScreen* qscreen) { + if (this->mScreen != nullptr) { + QObject::disconnect(this->mScreen, nullptr, this, nullptr); + } + + if (qscreen != nullptr) { + QObject::connect(qscreen, &QObject::destroyed, this, &SessionLockSurface::onScreenDestroyed); + } + + if (this->window == nullptr) { + this->mScreen = qscreen; + emit this->screenChanged(); + } else this->window->setScreen(qscreen); +} + +void SessionLockSurface::onScreenDestroyed() { this->mScreen = nullptr; } + +QColor SessionLockSurface::color() const { + if (this->window == nullptr) return this->mColor; + else return this->window->color(); +} + +void SessionLockSurface::setColor(QColor color) { + if (this->window == nullptr) { + this->mColor = color; + emit this->colorChanged(); + } else this->window->setColor(color); +} + +QQmlListProperty SessionLockSurface::data() { + return this->mContentItem->property("data").value>(); +} + +void SessionLockSurface::onWidthChanged() { this->mContentItem->setWidth(this->width()); } +void SessionLockSurface::onHeightChanged() { this->mContentItem->setHeight(this->height()); } diff --git a/src/wayland/session_lock.hpp b/src/wayland/session_lock.hpp new file mode 100644 index 0000000..2582aac --- /dev/null +++ b/src/wayland/session_lock.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/reload.hpp" +#include "session_lock/session_lock.hpp" + +class SessionLockSurface; + +/// Note: Very untested. Do anything outside of the obvious use cases and you WILL red screen. +class SessionLock: public Reloadable { + Q_OBJECT; + // clang-format off + /// Note: only one SessionLock may be locked at a time. + Q_PROPERTY(bool locked READ isLocked WRITE setLocked NOTIFY lockStateChanged); + /// Returns the *compositor* lock state, which will only be set to true after all surfaces are in place. + Q_PROPERTY(bool secure READ isSecure NOTIFY secureStateChanged); + /// Component that will be instantiated for each screen. Must be a `SessionLockSurface`. + Q_PROPERTY(QQmlComponent* surface READ surfaceComponent WRITE setSurfaceComponent NOTIFY surfaceComponentChanged); + // clang-format on + QML_ELEMENT; + Q_CLASSINFO("DefaultProperty", "surface"); + +public: + explicit SessionLock(QObject* parent = nullptr): Reloadable(parent) {} + + void onReload(QObject* oldInstance) override; + + [[nodiscard]] bool isLocked() const; + void setLocked(bool locked); + + [[nodiscard]] bool isSecure() const; + + [[nodiscard]] QQmlComponent* surfaceComponent() const; + void setSurfaceComponent(QQmlComponent* surfaceComponent); + + QSDOC_HIDE Q_INVOKABLE void rip(); + +signals: + void lockStateChanged(); + void secureStateChanged(); + void surfaceComponentChanged(); + +private slots: + void unlock(); + void onScreensChanged(); + +private: + void updateSurfaces(SessionLock* old = nullptr); + + SessionLockManager* manager = nullptr; + QQmlComponent* mSurfaceComponent = nullptr; + QMap surfaces; + bool lockTarget = false; + + friend class SessionLockSurface; +}; + +class SessionLockSurface: public Reloadable { + Q_OBJECT; + // clang-format off + Q_PROPERTY(QQuickItem* contentItem READ contentItem); + /// If the window has been presented yet. + Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged); + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + /// The screen that the window currently occupies. + /// + /// > [!INFO] This cannot be changed after windowConnected. + Q_PROPERTY(QuickshellScreenInfo* screen READ screen NOTIFY screenChanged); + /// The background color of the window. Defaults to white. + /// + /// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems. + /// > Using a colored content item over a transparent window is the recommended way to work around this: + /// > ```qml + /// > ProxyWindow { + /// > Rectangle { + /// > anchors.fill: parent + /// > color: "#20ffffff" + /// > + /// > // your content here + /// > } + /// > } + /// > ``` + /// > ... but you probably shouldn't make a transparent lock, + /// > and most compositors will ignore an attempt to do so. + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); + Q_PROPERTY(QQmlListProperty data READ data); + // clang-format on + QML_ELEMENT; + Q_CLASSINFO("DefaultProperty", "data"); + +public: + explicit SessionLockSurface(QObject* parent = nullptr); + ~SessionLockSurface() override; + Q_DISABLE_COPY_MOVE(SessionLockSurface); + + void onReload(QObject* oldInstance) override; + QQuickWindow* disownWindow(); + + void show(); + + [[nodiscard]] QQuickItem* contentItem() const; + + [[nodiscard]] bool isVisible() const; + + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + + [[nodiscard]] QuickshellScreenInfo* screen() const; + void setScreen(QScreen* qscreen); + + [[nodiscard]] QColor color() const; + void setColor(QColor color); + + [[nodiscard]] QQmlListProperty data(); + +signals: + void visibleChanged(); + void widthChanged(); + void heightChanged(); + void screenChanged(); + void colorChanged(); + +private slots: + void onScreenDestroyed(); + void onWidthChanged(); + void onHeightChanged(); + +private: + QQuickWindow* window = nullptr; + QQuickItem* mContentItem; + QScreen* mScreen = nullptr; + QColor mColor = Qt::white; + LockWindowExtension* ext; +}; diff --git a/src/wayland/session_lock/lock.cpp b/src/wayland/session_lock/lock.cpp index be64a4c..95b9c5c 100644 --- a/src/wayland/session_lock/lock.cpp +++ b/src/wayland/session_lock/lock.cpp @@ -17,10 +17,10 @@ QSWaylandSessionLock::~QSWaylandSessionLock() { this->unlock(); } void QSWaylandSessionLock::unlock() { if (this->isInitialized()) { - if (this->locked) this->unlock_and_destroy(); - else this->destroy(); + if (this->finished) this->destroy(); + else this->unlock_and_destroy(); - this->locked = false; + this->secure = false; this->manager->active = nullptr; emit this->unlocked(); @@ -29,14 +29,15 @@ void QSWaylandSessionLock::unlock() { bool QSWaylandSessionLock::active() const { return this->isInitialized(); } -bool QSWaylandSessionLock::hasCompositorLock() const { return this->locked; } +bool QSWaylandSessionLock::hasCompositorLock() const { return this->secure; } void QSWaylandSessionLock::ext_session_lock_v1_locked() { - this->locked = true; + this->secure = true; emit this->compositorLocked(); } void QSWaylandSessionLock::ext_session_lock_v1_finished() { - this->locked = false; + this->secure = false; + this->finished = true; this->unlock(); } diff --git a/src/wayland/session_lock/lock.hpp b/src/wayland/session_lock/lock.hpp index d28a735..a2ad1e6 100644 --- a/src/wayland/session_lock/lock.hpp +++ b/src/wayland/session_lock/lock.hpp @@ -34,5 +34,6 @@ private: QSWaylandSessionLockManager* manager; // static and not dealloc'd // true when the compositor determines the session is locked - bool locked = false; + bool secure = false; + bool finished = false; }; diff --git a/src/wayland/session_lock/session_lock.cpp b/src/wayland/session_lock/session_lock.cpp index 3ab4222..601795c 100644 --- a/src/wayland/session_lock/session_lock.cpp +++ b/src/wayland/session_lock/session_lock.cpp @@ -21,7 +21,7 @@ static QSWaylandSessionLockManager* manager() { } bool SessionLockManager::lock() { - if (SessionLockManager::sessionLocked()) return false; + if (this->isLocked() || SessionLockManager::sessionLocked()) return false; this->mLock = manager()->acquireLock(); this->mLock->setParent(this); diff --git a/src/wayland/session_lock/session_lock.hpp b/src/wayland/session_lock/session_lock.hpp index 78581ab..8c8f1f0 100644 --- a/src/wayland/session_lock/session_lock.hpp +++ b/src/wayland/session_lock/session_lock.hpp @@ -13,7 +13,7 @@ class SessionLockManager: public QObject { Q_OBJECT; public: - SessionLockManager(QObject* parent = nullptr): QObject(parent) {} + explicit SessionLockManager(QObject* parent = nullptr): QObject(parent) {} Q_DISABLE_COPY_MOVE(SessionLockManager); // Returns true if a lock was acquired. @@ -57,7 +57,7 @@ class LockWindowExtension: public QObject { Q_OBJECT; public: - LockWindowExtension(QObject* parent = nullptr): QObject(parent) {} + explicit LockWindowExtension(QObject* parent = nullptr): QObject(parent) {} ~LockWindowExtension() override; // Attach this lock extension to the given window. diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index ed409db..e9cbbf2 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -40,7 +40,10 @@ QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWayla this->init(this->ext->lock->get_lock_surface(window->waylandSurface()->object(), output)); } -QSWaylandSessionLockSurface::~QSWaylandSessionLockSurface() { this->destroy(); } +QSWaylandSessionLockSurface::~QSWaylandSessionLockSurface() { + if (this->ext != nullptr) this->ext->surface = nullptr; + this->destroy(); +} bool QSWaylandSessionLockSurface::isExposed() const { return this->configured; } @@ -60,7 +63,7 @@ bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { if (ext == nullptr) { - this->window()->window()->close(); + if (this->window() != nullptr) this->window()->window()->close(); } else { if (this->ext != nullptr) { this->ext->surface = nullptr;