diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ea2a5d8..cf4ebbc 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -117,6 +117,9 @@ endif() add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) +add_subdirectory(idle_notify) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/idle_notify/CMakeLists.txt b/src/wayland/idle_notify/CMakeLists.txt new file mode 100644 index 0000000..889c7ce --- /dev/null +++ b/src/wayland/idle_notify/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-idle-notify STATIC + proto.cpp + monitor.cpp +) + +qt_add_qml_module(quickshell-wayland-idle-notify + URI Quickshell.Wayland._IdleNotify + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-idle-notify) + +qs_add_module_deps_light(quickshell-wayland-idle-notify Quickshell) + +wl_proto(wlp-idle-notify ext-idle-notify-v1 "${WAYLAND_PROTOCOLS}/staging/ext-idle-notify") + +target_link_libraries(quickshell-wayland-idle-notify PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-idle-notify +) + +qs_module_pch(quickshell-wayland-idle-notify SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-idle-notifyplugin) diff --git a/src/wayland/idle_notify/monitor.cpp b/src/wayland/idle_notify/monitor.cpp new file mode 100644 index 0000000..e830d4b --- /dev/null +++ b/src/wayland/idle_notify/monitor.cpp @@ -0,0 +1,52 @@ +#include "monitor.hpp" +#include + +#include +#include +#include + +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +IdleMonitor::~IdleMonitor() { delete this->bNotification.value(); } + +void IdleMonitor::onPostReload() { + this->bParams.setBinding([this] { + return Params { + .enabled = this->bEnabled.value(), + .timeout = this->bTimeout.value(), + .respectInhibitors = this->bRespectInhibitors.value() + }; + }); + + this->bIsIdle.setBinding([this] { + auto* notification = this->bNotification.value(); + return notification ? notification->bIsIdle.value() : false; + }); +} + +void IdleMonitor::updateNotification() { + auto* notification = this->bNotification.value(); + delete notification; + notification = nullptr; + + auto guard = qScopeGuard([&] { this->bNotification = notification; }); + + auto params = this->bParams.value(); + + if (params.enabled) { + auto* manager = impl::IdleNotificationManager::instance(); + + if (!manager) { + qWarning() << "Cannot create idle monitor as ext-idle-notify-v1 is not supported by the " + "current compositor."; + return; + } + + auto timeout = static_cast(std::max(0, static_cast(params.timeout * 1000))); + notification = manager->createIdleNotification(timeout, params.respectInhibitors); + } +} + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/monitor.hpp b/src/wayland/idle_notify/monitor.hpp new file mode 100644 index 0000000..25bd5a6 --- /dev/null +++ b/src/wayland/idle_notify/monitor.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/reload.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +///! Provides a notification when a wayland session is makred idle +/// An idle monitor detects when the user stops providing input for a period of time. +/// +/// > [!NOTE] Using an idle monitor requires the compositor support the [ext-idle-notify-v1] protocol. +/// +/// [ext-idle-notify-v1]: https://wayland.app/protocols/ext-idle-notify-v1 +class IdleMonitor: public PostReloadHook { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the idle monitor should be enabled. Defaults to true. + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged); + /// The amount of time in seconds the idle monitor should wait before reporting an idle state. + /// + /// Defaults to zero, which reports idle status immediately. + Q_PROPERTY(qreal timeout READ default WRITE default NOTIFY timeoutChanged BINDABLE bindableTimeout); + /// When set to true, @@isIdle will depend on both user interaction and active idle inhibitors. + /// When false, the value will depend solely on user interaction. Defaults to true. + Q_PROPERTY(bool respectInhibitors READ default WRITE default NOTIFY respectInhibitorsChanged BINDABLE bindableRespectInhibitors); + /// This property is true if the user has been idle for at least @@timeout. + /// What is considered to be idle is influenced by @@respectInhibitors. + Q_PROPERTY(bool isIdle READ default NOTIFY isIdleChanged BINDABLE bindableIsIdle); + // clang-format on + +public: + IdleMonitor() = default; + ~IdleMonitor() override; + Q_DISABLE_COPY_MOVE(IdleMonitor); + + void onPostReload() override; + + [[nodiscard]] bool isEnabled() const { return this->bNotification.value(); } + void setEnabled(bool enabled) { this->bEnabled = enabled; } + + [[nodiscard]] QBindable bindableTimeout() { return &this->bTimeout; } + [[nodiscard]] QBindable bindableRespectInhibitors() { return &this->bRespectInhibitors; } + [[nodiscard]] QBindable bindableIsIdle() const { return &this->bIsIdle; } + +signals: + void enabledChanged(); + void timeoutChanged(); + void respectInhibitorsChanged(); + void isIdleChanged(); + +private: + void updateNotification(); + + struct Params { + bool enabled; + qreal timeout; + bool respectInhibitors; + }; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bEnabled, true, &IdleMonitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, qreal, bTimeout, &IdleMonitor::timeoutChanged); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bRespectInhibitors, true, &IdleMonitor::respectInhibitorsChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, Params, bParams, &IdleMonitor::updateNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, impl::IdleNotification*, bNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, bool, bIsIdle, &IdleMonitor::isIdleChanged); + // clang-format on +}; + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/proto.cpp b/src/wayland/idle_notify/proto.cpp new file mode 100644 index 0000000..9b3fa81 --- /dev/null +++ b/src/wayland/idle_notify/proto.cpp @@ -0,0 +1,75 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_LOGGING_CATEGORY(logIdleNotify, "quickshell.wayland.idle_notify", QtWarningMsg); +} + +namespace qs::wayland::idle_notify::impl { + +IdleNotificationManager::IdleNotificationManager(): QWaylandClientExtensionTemplate(2) { + this->initialize(); +} + +IdleNotificationManager* IdleNotificationManager::instance() { + static auto* instance = new IdleNotificationManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +IdleNotification* +IdleNotificationManager::createIdleNotification(quint32 timeout, bool respectInhibitors) { + if (!respectInhibitors + && this->QtWayland::ext_idle_notifier_v1::version() + < EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION_SINCE_VERSION) + { + qCWarning(logIdleNotify) << "Cannot ignore inhibitors for new idle notifier: Compositor does " + "not support protocol version 2."; + + respectInhibitors = true; + } + + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + if (inputDevice == nullptr) { + qCCritical(logIdleNotify) << "Could not create idle notifier: No seat."; + return nullptr; + } + + ::ext_idle_notification_v1* notification = nullptr; + if (respectInhibitors) notification = this->get_idle_notification(timeout, inputDevice->object()); + else notification = this->get_input_idle_notification(timeout, inputDevice->object()); + + auto* wrapper = new IdleNotification(notification); + qCDebug(logIdleNotify) << "Created" << wrapper << "with timeout:" << timeout + << "respects inhibitors:" << respectInhibitors; + return wrapper; +} + +IdleNotification::~IdleNotification() { + qCDebug(logIdleNotify) << "Destroyed" << this; + if (this->isInitialized()) this->destroy(); +} + +void IdleNotification::ext_idle_notification_v1_idled() { + qCDebug(logIdleNotify) << this << "has been marked idle"; + this->bIsIdle = true; +} + +void IdleNotification::ext_idle_notification_v1_resumed() { + qCDebug(logIdleNotify) << this << "has been marked resumed"; + this->bIsIdle = false; +} + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/proto.hpp b/src/wayland/idle_notify/proto.hpp new file mode 100644 index 0000000..11dbf29 --- /dev/null +++ b/src/wayland/idle_notify/proto.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_DECLARE_LOGGING_CATEGORY(logIdleNotify); +} + +namespace qs::wayland::idle_notify::impl { + +class IdleNotification; + +class IdleNotificationManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_idle_notifier_v1 { +public: + explicit IdleNotificationManager(); + IdleNotification* createIdleNotification(quint32 timeout, bool respectInhibitors); + + static IdleNotificationManager* instance(); +}; + +class IdleNotification + : public QObject + , public QtWayland::ext_idle_notification_v1 { + Q_OBJECT; + +public: + explicit IdleNotification(::ext_idle_notification_v1* notification) + : QtWayland::ext_idle_notification_v1(notification) {} + + ~IdleNotification() override; + Q_DISABLE_COPY_MOVE(IdleNotification); + + Q_OBJECT_BINDABLE_PROPERTY(IdleNotification, bool, bIsIdle); + +protected: + void ext_idle_notification_v1_idled() override; + void ext_idle_notification_v1_resumed() override; +}; + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/test/manual/idle_notify.qml b/src/wayland/idle_notify/test/manual/idle_notify.qml new file mode 100644 index 0000000..3bf6cbd --- /dev/null +++ b/src/wayland/idle_notify/test/manual/idle_notify.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + color: contentItem.palette.window + + IdleMonitor { + id: monitor + enabled: enabledCb.checked + timeout: timeoutSb.value + respectInhibitors: respectInhibitorsCb.checked + } + + ColumnLayout { + Label { text: `Is idle? ${monitor.isIdle}` } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: true + } + + CheckBox { + id: respectInhibitorsCb + text: "Respect Inhibitors" + checked: true + } + + RowLayout { + Label { text: "Timeout" } + + SpinBox { + id: timeoutSb + editable: true + from: 0 + to: 1000 + value: 5 + } + } + } +} diff --git a/src/wayland/module.md b/src/wayland/module.md index 622346a..0216e6d 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -6,5 +6,6 @@ headers = [ "toplevel_management/qml.hpp", "screencopy/view.hpp", "idle_inhibit/inhibitor.hpp", + "idle_notify/monitor.hpp", ] -----