diff --git a/changelog/next.md b/changelog/next.md index aaeaf1cc..f83eba3d 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -1,3 +1,7 @@ +## New Features + +- Added opt-in logind LockedHint updates for WlSessionLock. + ## Bug Fixes - Fixed ScreencopyView not displaying when only lock surfaces are shown. diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index fc004f3d..9c88ba0f 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -26,11 +26,10 @@ qt_add_library(quickshell-dbus STATIC # dbus headers target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) - target_link_libraries(quickshell-dbus PRIVATE Qt::Core Qt::DBus) + # todo: link dbus to quickshell here instead of in modules that use it directly # linker does not like this as is - qs_add_pchset(dbus DEPENDENCIES Qt::DBus HEADERS diff --git a/src/dbus/objectmanager.hpp b/src/dbus/objectmanager.hpp index 4246ea28..ea88d499 100644 --- a/src/dbus/objectmanager.hpp +++ b/src/dbus/objectmanager.hpp @@ -34,4 +34,4 @@ class DBusObjectManager: public QObject { DBusObjectManagerInterface* mInterface = nullptr; }; -} // namespace qs::dbus \ No newline at end of file +} // namespace qs::dbus diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index 3b908b8e..c81e9b3f 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -22,15 +22,35 @@ #include "../window/proxywindow.hpp" #include "session_lock/session_lock.hpp" +bool WlSessionLock::exposeDbus() const { return this->mExposeDbus; } + +void WlSessionLock::setExposeDbus(bool exposeDbus) { + if (this->mExposeDbus == exposeDbus) return; + if (this->manager != nullptr) { + qWarning() << "WlSessionLock.exposeDbus cannot be changed after initialization."; + return; + } + this->mExposeDbus = exposeDbus; + emit this->exposeDbusChanged(); +} + void WlSessionLock::onReload(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); - if (old != nullptr) { + auto* app = QCoreApplication::instance(); + auto* guiApp = qobject_cast(app); + + if (old != nullptr && guiApp != nullptr) { + // Ensure we don't leave stale connections to the old instance behind. + QObject::disconnect(guiApp, nullptr, old, nullptr); + } + + if (old != nullptr && old->manager != nullptr) { QObject::disconnect(old->manager, nullptr, old, nullptr); this->manager = old->manager; this->manager->setParent(this); } else { - this->manager = new SessionLockManager(this); + this->manager = new SessionLockManager(this->mExposeDbus, this); } // clang-format off @@ -39,9 +59,6 @@ void WlSessionLock::onReload(QObject* oldInstance) { QObject::connect(this->manager, &SessionLockManager::unlocked, this, &WlSessionLock::unlock); - auto* app = QCoreApplication::instance(); - auto* guiApp = qobject_cast(app); - if (guiApp != nullptr) { QObject::connect(guiApp, &QGuiApplication::primaryScreenChanged, this, &WlSessionLock::onScreensChanged); QObject::connect(guiApp, &QGuiApplication::screenAdded, this, &WlSessionLock::onScreensChanged); @@ -99,7 +116,7 @@ void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { } if (show) { - if (!this->manager->isLocked()) { + if (this->manager == nullptr || !this->manager->isLocked()) { qFatal() << "Tried to show lockscreen surfaces without active lock"; } @@ -126,9 +143,13 @@ void WlSessionLock::realizeLockTarget(WlSessionLock* old) { // preload initial surfaces to make the chance of the compositor displaying a blank // frame before the lock surfaces are shown as low as possible. - this->updateSurfaces(false); + this->updateSurfaces(false, old); - if (!this->manager->lock()) this->lockTarget = false; + if (this->manager == nullptr || !this->manager->lock()) { + this->lockTarget = false; + this->unlock(); + return; + } this->updateSurfaces(true, old); } else { @@ -137,18 +158,22 @@ void WlSessionLock::realizeLockTarget(WlSessionLock* old) { } void WlSessionLock::unlock() { - if (this->isLocked()) { - this->lockTarget = false; - this->manager->unlock(); + if (!this->isLocked()) return; - for (auto* surface: this->surfaces) { - surface->deleteLater(); - } + this->lockTarget = false; - this->surfaces.clear(); + // Manager may legitimately be null during early reload/init paths. + if (this->manager != nullptr) { + this->manager->unlock(); + } - emit this->lockStateChanged(); + for (auto* surface: this->surfaces) { + surface->deleteLater(); } + + this->surfaces.clear(); + + emit this->lockStateChanged(); } void WlSessionLock::onScreensChanged() { diff --git a/src/wayland/session_lock.hpp b/src/wayland/session_lock.hpp index 9a34eafa..a0e51bc6 100644 --- a/src/wayland/session_lock.hpp +++ b/src/wayland/session_lock.hpp @@ -67,6 +67,7 @@ class WlSessionLock: public Reloadable { Q_PROPERTY(bool secure READ isSecure NOTIFY secureStateChanged); /// The surface that will be created for each screen. Must create a @@WlSessionLockSurface$. Q_PROPERTY(QQmlComponent* surface READ surfaceComponent WRITE setSurfaceComponent NOTIFY surfaceComponentChanged); + Q_PROPERTY(bool exposeDbus READ exposeDbus WRITE setExposeDbus NOTIFY exposeDbusChanged); // clang-format on QML_ELEMENT; Q_CLASSINFO("DefaultProperty", "surface"); @@ -84,10 +85,14 @@ class WlSessionLock: public Reloadable { [[nodiscard]] QQmlComponent* surfaceComponent() const; void setSurfaceComponent(QQmlComponent* surfaceComponent); + [[nodiscard]] bool exposeDbus() const; + void setExposeDbus(bool exposeDbus); + signals: void lockStateChanged(); void secureStateChanged(); void surfaceComponentChanged(); + void exposeDbusChanged(); private slots: void unlock(); @@ -101,6 +106,7 @@ private slots: QQmlComponent* mSurfaceComponent = nullptr; QMap surfaces; bool lockTarget = false; + bool mExposeDbus = false; friend class WlSessionLockSurface; }; @@ -132,7 +138,6 @@ class WlSessionLockSurface: public Reloadable { /// > // 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); diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt index d157fc13..b5c47382 100644 --- a/src/wayland/session_lock/CMakeLists.txt +++ b/src/wayland/session_lock/CMakeLists.txt @@ -5,11 +5,10 @@ qt_add_library(quickshell-wayland-sessionlock STATIC shell_integration.cpp session_lock.cpp ) - wl_proto(wlp-session-lock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock") target_link_libraries(quickshell-wayland-sessionlock PRIVATE - Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate Qt::DBus wayland-client wlp-session-lock ) diff --git a/src/wayland/session_lock/lock.hpp b/src/wayland/session_lock/lock.hpp index a2ad1e6f..3ac7dcea 100644 --- a/src/wayland/session_lock/lock.hpp +++ b/src/wayland/session_lock/lock.hpp @@ -6,7 +6,7 @@ class QSWaylandSessionLockManager; -class QSWaylandSessionLock +class QSWaylandSessionLock // NOLINT(misc-multiple-inheritance) : public QObject , public QtWayland::ext_session_lock_v1 { Q_OBJECT; @@ -27,10 +27,11 @@ class QSWaylandSessionLock void compositorLocked(); void unlocked(); -private: +protected: void ext_session_lock_v1_locked() override; void ext_session_lock_v1_finished() override; +private: QSWaylandSessionLockManager* manager; // static and not dealloc'd // true when the compositor determines the session is locked diff --git a/src/wayland/session_lock/manager.hpp b/src/wayland/session_lock/manager.hpp index 6a21a3ce..9a92e002 100644 --- a/src/wayland/session_lock/manager.hpp +++ b/src/wayland/session_lock/manager.hpp @@ -6,7 +6,7 @@ #include "lock.hpp" -class QSWaylandSessionLockManager +class QSWaylandSessionLockManager // NOLINT(misc-multiple-inheritance) : public QWaylandClientExtensionTemplate , public QtWayland::ext_session_lock_manager_v1 { public: diff --git a/src/wayland/session_lock/session_lock.cpp b/src/wayland/session_lock/session_lock.cpp index c32dd90a..d13ce41f 100644 --- a/src/wayland/session_lock/session_lock.cpp +++ b/src/wayland/session_lock/session_lock.cpp @@ -1,16 +1,29 @@ #include "session_lock.hpp" #include +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include +#include #include +#include "../../core/logcat.hpp" #include "lock.hpp" #include "manager.hpp" #include "shell_integration.hpp" #include "surface.hpp" namespace { +QS_LOGGING_CATEGORY(logSessionLock, "quickshell.wayland.sessionlock", QtWarningMsg); + QSWaylandSessionLockManager* manager() { static QSWaylandSessionLockManager* manager = nullptr; // NOLINT @@ -20,35 +33,140 @@ QSWaylandSessionLockManager* manager() { return manager; } + } // namespace +SessionLockManager::SessionLockManager(bool logindLockedHint, QObject* parent) + : QObject(parent) + , mLogindLockedHint(logindLockedHint) {} + bool SessionLockManager::lockAvailable() { return manager()->isActive(); } bool SessionLockManager::lock() { if (this->isLocked() || 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); + QObject::connect(this->mLock, &QSWaylandSessionLock::compositorLocked, this, [this]() { + this->updateLogindLockedHint(true); + emit this->locked(); + }); + + QObject::connect(this->mLock, &QSWaylandSessionLock::unlocked, this, [this]() { + this->updateLogindLockedHint(false); + emit this->unlocked(); + }); // clang-format on + return true; } bool SessionLockManager::unlock() { if (!this->isLocked()) return false; + this->mLock->unlock(); + 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(); } + bool SessionLockManager::isSecure() { return manager()->isSecure(); } +void SessionLockManager::updateLogindLockedHint(bool locked) { + if (!this->mLogindLockedHint) return; + + auto generation = ++this->mLogindLockedHintGeneration; + + if (!this->mLogindSessionPath.isEmpty()) { + this->updateLogindLockedHint(this->mLogindSessionPath, locked, generation); + return; + } + + auto sessionId = qEnvironmentVariable("XDG_SESSION_ID"); + if (sessionId.isEmpty()) { + qCWarning(logSessionLock) << "Cannot set logind LockedHint: XDG_SESSION_ID is unset."; + return; + } + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning(logSessionLock) << "Cannot set logind LockedHint: system bus is not connected."; + return; + } + + auto message = QDBusMessage::createMethodCall( + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "GetSession" + ); + + message << sessionId; + + auto* call = new QDBusPendingCallWatcher(bus.asyncCall(message), this); + QObject::connect( + call, + &QDBusPendingCallWatcher::finished, + this, + [this, locked, generation](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(logSessionLock) + << "Failed to resolve logind session:" << reply.error().message(); + } else if (generation == this->mLogindLockedHintGeneration) { + this->mLogindSessionPath = reply.value().path(); + this->updateLogindLockedHint(this->mLogindSessionPath, locked, generation); + } + + delete call; + } + ); +} + +void SessionLockManager::updateLogindLockedHint( + const QString& sessionPath, + bool locked, + quint64 generation +) { + if (generation != this->mLogindLockedHintGeneration) return; + + auto message = QDBusMessage::createMethodCall( + "org.freedesktop.login1", + sessionPath, + "org.freedesktop.login1.Session", + "SetLockedHint" + ); + + message << locked; + + auto* call = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(message), this); + QObject::connect( + call, + &QDBusPendingCallWatcher::finished, + this, + [locked](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + if (reply.isError()) { + qCWarning(logSessionLock) + << "Failed to set logind LockedHint to" << locked << ':' << reply.error().message(); + } else { + qCDebug(logSessionLock) << "Set logind LockedHint to" << locked; + } + + delete call; + } + ); +} + LockWindowExtension::~LockWindowExtension() { if (this->surface != nullptr) { this->surface->setExtension(nullptr); @@ -83,14 +201,17 @@ bool LockWindowExtension::attach(QWindow* window, SessionLockManager* manager) { window->setScreen(screen); 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; // NOLINT + if (lockIntegration == nullptr) { lockIntegration = new QSWaylandSessionLockIntegration(); + if (!lockIntegration->initialize(waylandWindow->display())) { delete lockIntegration; lockIntegration = nullptr; @@ -103,6 +224,7 @@ bool LockWindowExtension::attach(QWindow* window, SessionLockManager* manager) { this->setParent(window); window->setProperty("sessionlock_ext", QVariant::fromValue(this)); + this->lock = manager->mLock; if (waylandWindow != nullptr) { diff --git a/src/wayland/session_lock/session_lock.hpp b/src/wayland/session_lock/session_lock.hpp index f35289f1..d3e25154 100644 --- a/src/wayland/session_lock/session_lock.hpp +++ b/src/wayland/session_lock/session_lock.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include #include class QSWaylandSessionLock; @@ -10,19 +12,14 @@ class QSWaylandSessionLockSurface; class QSWaylandSessionLockIntegration; class SessionLockManager: public QObject { - Q_OBJECT; + Q_OBJECT public: - explicit SessionLockManager(QObject* parent = nullptr): QObject(parent) {} + explicit SessionLockManager(bool logindLockedHint, QObject* parent = nullptr); [[nodiscard]] static bool lockAvailable(); - // 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; @@ -46,43 +43,36 @@ class SessionLockManager: public QObject { // After receiving this event the caller should destroy all of its lock surfaces. void unlocked(); -private slots: - //void onUnlocked(); - private: + void updateLogindLockedHint(bool locked); + void updateLogindLockedHint(const QString& sessionPath, bool locked, quint64 generation); + QSWaylandSessionLock* mLock = nullptr; + QString mLogindSessionPath; + quint64 mLogindLockedHintGeneration = 0; + bool mLogindLockedHint = false; friend class LockWindowExtension; }; class LockWindowExtension: public QObject { - Q_OBJECT; + Q_OBJECT public: explicit LockWindowExtension(QObject* parent = nullptr): QObject(parent) {} ~LockWindowExtension() override; - Q_DISABLE_COPY_MOVE(LockWindowExtension); + Q_DISABLE_COPY_MOVE(LockWindowExtension) [[nodiscard]] bool isAttached() const; - // 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(); 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: diff --git a/src/wayland/session_lock/surface.hpp b/src/wayland/session_lock/surface.hpp index 39be88df..72085741 100644 --- a/src/wayland/session_lock/surface.hpp +++ b/src/wayland/session_lock/surface.hpp @@ -11,7 +11,7 @@ #include "session_lock.hpp" -class QSWaylandSessionLockSurface +class QSWaylandSessionLockSurface // NOLINT(misc-multiple-inheritance) : public QtWaylandClient::QWaylandShellSurface , public QtWayland::ext_session_lock_surface_v1 { public: @@ -31,10 +31,11 @@ class QSWaylandSessionLockSurface void setExtension(LockWindowExtension* ext); void setVisible(); -private: +protected: void ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; +private: #if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) void initVisible(); bool visible = false;