feat(slock): add user facing SessionLock and SessionLockSurface

This commit is contained in:
outfoxxed 2024-02-28 19:55:49 -08:00
parent 1fa87b7c5a
commit 48bdcf4db2
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
10 changed files with 458 additions and 14 deletions

View file

@ -134,8 +134,10 @@ void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) {
QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); QObject::connect(qscreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed);
} }
if (this->window == nullptr) this->mScreen = qscreen; if (this->window == nullptr) {
else this->window->setScreen(qscreen); this->mScreen = qscreen;
emit this->screenChanged();
} else this->window->setScreen(qscreen);
} }
void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; }

View file

@ -46,6 +46,7 @@ endfunction()
qt_add_library(quickshell-wayland STATIC qt_add_library(quickshell-wayland STATIC
wlr_layershell.cpp wlr_layershell.cpp
session_lock.cpp
) )
qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland) qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland)

View file

@ -3,5 +3,6 @@ description = "Wayland specific Quickshell types"
headers = [ headers = [
"wlr_layershell/window.hpp", "wlr_layershell/window.hpp",
"wlr_layershell.hpp", "wlr_layershell.hpp",
"session_lock.hpp",
] ]
----- -----

View file

@ -0,0 +1,284 @@
#include "session_lock.hpp"
#include <qcolor.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "session_lock/session_lock.hpp"
void SessionLock::onReload(QObject* oldInstance) {
auto* old = qobject_cast<SessionLock*>(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<QGuiApplication*>(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<SessionLockSurface*>(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<SessionLockSurface*>(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<SessionLock*>(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<SessionLockSurface*>(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<QObject> SessionLockSurface::data() {
return this->mContentItem->property("data").value<QQmlListProperty<QObject>>();
}
void SessionLockSurface::onWidthChanged() { this->mContentItem->setWidth(this->width()); }
void SessionLockSurface::onHeightChanged() { this->mContentItem->setHeight(this->height()); }

View file

@ -0,0 +1,151 @@
#pragma once
#include <qcolor.h>
#include <qcontainerfwd.h>
#include <qguiapplication.h>
#include <qmap.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qscreen.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<QScreen*, SessionLockSurface*> 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<QObject> 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<QObject> 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;
};

View file

@ -17,10 +17,10 @@ QSWaylandSessionLock::~QSWaylandSessionLock() { this->unlock(); }
void QSWaylandSessionLock::unlock() { void QSWaylandSessionLock::unlock() {
if (this->isInitialized()) { if (this->isInitialized()) {
if (this->locked) this->unlock_and_destroy(); if (this->finished) this->destroy();
else this->destroy(); else this->unlock_and_destroy();
this->locked = false; this->secure = false;
this->manager->active = nullptr; this->manager->active = nullptr;
emit this->unlocked(); emit this->unlocked();
@ -29,14 +29,15 @@ void QSWaylandSessionLock::unlock() {
bool QSWaylandSessionLock::active() const { return this->isInitialized(); } 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() { void QSWaylandSessionLock::ext_session_lock_v1_locked() {
this->locked = true; this->secure = true;
emit this->compositorLocked(); emit this->compositorLocked();
} }
void QSWaylandSessionLock::ext_session_lock_v1_finished() { void QSWaylandSessionLock::ext_session_lock_v1_finished() {
this->locked = false; this->secure = false;
this->finished = true;
this->unlock(); this->unlock();
} }

View file

@ -34,5 +34,6 @@ private:
QSWaylandSessionLockManager* manager; // static and not dealloc'd QSWaylandSessionLockManager* manager; // static and not dealloc'd
// true when the compositor determines the session is locked // true when the compositor determines the session is locked
bool locked = false; bool secure = false;
bool finished = false;
}; };

View file

@ -21,7 +21,7 @@ static QSWaylandSessionLockManager* manager() {
} }
bool SessionLockManager::lock() { bool SessionLockManager::lock() {
if (SessionLockManager::sessionLocked()) return false; if (this->isLocked() || SessionLockManager::sessionLocked()) return false;
this->mLock = manager()->acquireLock(); this->mLock = manager()->acquireLock();
this->mLock->setParent(this); this->mLock->setParent(this);

View file

@ -13,7 +13,7 @@ class SessionLockManager: public QObject {
Q_OBJECT; Q_OBJECT;
public: public:
SessionLockManager(QObject* parent = nullptr): QObject(parent) {} explicit SessionLockManager(QObject* parent = nullptr): QObject(parent) {}
Q_DISABLE_COPY_MOVE(SessionLockManager); Q_DISABLE_COPY_MOVE(SessionLockManager);
// Returns true if a lock was acquired. // Returns true if a lock was acquired.
@ -57,7 +57,7 @@ class LockWindowExtension: public QObject {
Q_OBJECT; Q_OBJECT;
public: public:
LockWindowExtension(QObject* parent = nullptr): QObject(parent) {} explicit LockWindowExtension(QObject* parent = nullptr): QObject(parent) {}
~LockWindowExtension() override; ~LockWindowExtension() override;
// Attach this lock extension to the given window. // Attach this lock extension to the given window.

View file

@ -40,7 +40,10 @@ QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWayla
this->init(this->ext->lock->get_lock_surface(window->waylandSurface()->object(), output)); 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; } bool QSWaylandSessionLockSurface::isExposed() const { return this->configured; }
@ -60,7 +63,7 @@ bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) {
void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) {
if (ext == nullptr) { if (ext == nullptr) {
this->window()->window()->close(); if (this->window() != nullptr) this->window()->window()->close();
} else { } else {
if (this->ext != nullptr) { if (this->ext != nullptr) {
this->ext->surface = nullptr; this->ext->surface = nullptr;