diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index a96fe6bb..ca49c8f7 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -120,6 +120,9 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) add_subdirectory(idle_notify) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) +add_subdirectory(shortcuts_inhibit) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/shortcuts_inhibit/CMakeLists.txt b/src/wayland/shortcuts_inhibit/CMakeLists.txt new file mode 100644 index 00000000..8dedd3d5 --- /dev/null +++ b/src/wayland/shortcuts_inhibit/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-shortcuts-inhibit STATIC + proto.cpp + inhibitor.cpp +) + +qt_add_qml_module(quickshell-wayland-shortcuts-inhibit + URI Quickshell.Wayland._ShortcutsInhibitor + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-shortcuts-inhibit) + +qs_add_module_deps_light(quickshell-wayland-shortcuts-inhibit Quickshell) + +wl_proto(wlp-shortcuts-inhibit keyboard-shortcuts-inhibit-unstable-v1 "${WAYLAND_PROTOCOLS}/unstable/keyboard-shortcuts-inhibit") + +target_link_libraries(quickshell-wayland-shortcuts-inhibit PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-shortcuts-inhibit +) + +qs_module_pch(quickshell-wayland-shortcuts-inhibit SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-shortcuts-inhibitplugin) \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/inhibitor.cpp b/src/wayland/shortcuts_inhibit/inhibitor.cpp new file mode 100644 index 00000000..d4ba81b8 --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -0,0 +1,180 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { +using QtWaylandClient::QWaylandWindow; + +ShortcutsInhibitor::ShortcutsInhibitor() { + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); +} + +ShortcutsInhibitor::~ShortcutsInhibitor() { + if (!this->inhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + QObject::disconnect(this->inhibitor, nullptr, this, nullptr); + manager->unrefShortcutsInhibitor(this->inhibitor); +} + +QObject* ShortcutsInhibitor::window() const { return this->bWindowObject; } + +void ShortcutsInhibitor::setWindow(QObject* window) { + if (window == this->bWindowObject) return; + + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + this->bWindowObject = proxyWindow ? window : nullptr; +} + +bool ShortcutsInhibitor::isActive() const { + return this->inhibitor ? this->inhibitor->isActive() : false; +} + +void ShortcutsInhibitor::boundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect( + proxyWindow, + &QObject::destroyed, + this, + &ShortcutsInhibitor::onWindowDestroyed + ); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &ShortcutsInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } + + emit this->windowChanged(); +} + +void ShortcutsInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + this->bWindowObject = nullptr; +} + +void ShortcutsInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &ShortcutsInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &ShortcutsInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &ShortcutsInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void ShortcutsInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void ShortcutsInhibitor::onWaylandSurfaceCreated() { + auto* manager = impl::ShortcutsInhibitManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable shortcuts inhibitor as keyboard-shortcuts-inhibit-unstable-v1 is " + "not supported by " + "the current compositor."; + return; + } + + if (this->inhibitor) { + QObject::disconnect(this->inhibitor, nullptr, this, nullptr); + manager->unrefShortcutsInhibitor(this->inhibitor); + this->inhibitor = nullptr; + } + this->inhibitor = manager->createShortcutsInhibitor(this->mWaylandWindow); + + if (this->inhibitor) { + QObject::connect( + this->inhibitor, + &impl::ShortcutsInhibitor::activeChanged, + this, + &ShortcutsInhibitor::activeChanged + ); + } +} + +void ShortcutsInhibitor::onWaylandSurfaceDestroyed() { + if (!this->inhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + QObject::disconnect(this->inhibitor, nullptr, this, nullptr); + auto wasActive = this->inhibitor->isActive(); + manager->unrefShortcutsInhibitor(this->inhibitor); + this->inhibitor = nullptr; + + if (wasActive) { emit this->activeChanged(); } +} + +} // namespace qs::wayland::shortcuts_inhibit \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/inhibitor.hpp b/src/wayland/shortcuts_inhibit/inhibitor.hpp new file mode 100644 index 00000000..678f995b --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { + +///! Prevents compositor keyboard shortcuts from being triggered +/// A shortcuts inhibitor prevents the compositor from processing its own keyboard shortcuts +/// for the focused surface. This allows applications to receive key events for shortcuts +/// that would normally be handled by the compositor. +/// +/// The inhibitor only takes effect when the associated window is focused and the inhibitor +/// is enabled. The compositor may choose to ignore inhibitor requests based on its policy. +/// +/// > [!NOTE] Using a shortcuts inhibitor requires the compositor support the [keyboard-shortcuts-inhibit-unstable-v1] protocol. +/// +/// [keyboard-shortcuts-inhibit-unstable-v1]: https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1 +class ShortcutsInhibitor: public QObject { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the shortcuts inhibitor should be enabled. Defaults to false. + Q_PROPERTY(bool enabled READ default WRITE default NOTIFY enabledChanged BINDABLE bindableEnabled); + /// The window to associate the shortcuts inhibitor with. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); + /// Whether the inhibitor is currently active. The inhibitor is only active when the + /// associated window has keyboard focus and the compositor has granted the request. + /// + /// The compositor may deactivate the inhibitor if the user requests normal shortcuts to be restored. + /// When inactive, there is no way to programmatically reactivate it - the user must do so through + /// compositor-specific mechanisms. + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged); + // clang-format on + +public: + ShortcutsInhibitor(); + ~ShortcutsInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutsInhibitor); + + [[nodiscard]] QObject* window() const; + void setWindow(QObject* window); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + [[nodiscard]] bool isActive() const; + +signals: + void enabledChanged(); + void windowChanged(); + void activeChanged(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + +private: + void boundWindowChanged(); + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + impl::ShortcutsInhibitor* inhibitor = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, bool, bEnabled, &ShortcutsInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, QObject*, bWindowObject, &ShortcutsInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, QObject*, bBoundWindow, &ShortcutsInhibitor::boundWindowChanged); + // clang-format on +}; + +} // namespace qs::wayland::shortcuts_inhibit \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/proto.cpp b/src/wayland/shortcuts_inhibit/proto.cpp new file mode 100644 index 00000000..00e185a6 --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.cpp @@ -0,0 +1,98 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::shortcuts_inhibit::impl { + +namespace { +QS_LOGGING_CATEGORY(logShortcutsInhibit, "quickshell.wayland.shortcuts_inhibit", QtWarningMsg); +} + +ShortcutsInhibitManager::ShortcutsInhibitManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +ShortcutsInhibitManager* ShortcutsInhibitManager::instance() { + static auto* instance = new ShortcutsInhibitManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +ShortcutsInhibitor* +ShortcutsInhibitManager::createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + + if (inputDevice == nullptr) { + qCCritical(logShortcutsInhibit) << "Could not create shortcuts inhibitor: No seat."; + return nullptr; + } + + auto* wlSurface = surface->surface(); + + if (this->inhibitors.contains(wlSurface)) { + auto* inhibitor = this->inhibitors.value(wlSurface); + this->refCounts[inhibitor]++; + qCDebug(logShortcutsInhibit) << "Reusing existing inhibitor" << inhibitor << "for surface" + << wlSurface << "- refcount:" << this->refCounts[inhibitor]; + return inhibitor; + } + + auto* inhibitor = + new ShortcutsInhibitor(this->inhibit_shortcuts(wlSurface, inputDevice->object()), wlSurface); + this->inhibitors.insert(wlSurface, inhibitor); + this->refCounts.insert(inhibitor, 1); + qCDebug(logShortcutsInhibit) << "Created inhibitor" << inhibitor << "for surface" << wlSurface; + return inhibitor; +} + +void ShortcutsInhibitManager::refShortcutsInhibitor(ShortcutsInhibitor* inhibitor) { + if (inhibitor && this->refCounts.contains(inhibitor)) { + this->refCounts[inhibitor]++; + qCDebug(logShortcutsInhibit) << "Incremented refcount for inhibitor" << inhibitor + << "- refcount:" << this->refCounts[inhibitor]; + } +} + +void ShortcutsInhibitManager::unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor) { + if (!inhibitor || !this->refCounts.contains(inhibitor)) return; + + this->refCounts[inhibitor]--; + qCDebug(logShortcutsInhibit) << "Decremented refcount for inhibitor" << inhibitor + << "- refcount:" << this->refCounts[inhibitor]; + + if (this->refCounts[inhibitor] <= 0) { + qCDebug(logShortcutsInhibit) << "Refcount reached 0, destroying inhibitor" << inhibitor; + this->inhibitors.remove(inhibitor->surface()); + this->refCounts.remove(inhibitor); + delete inhibitor; + } +} + +ShortcutsInhibitor::~ShortcutsInhibitor() { + qCDebug(logShortcutsInhibit) << "Destroying inhibitor" << this << "for surface" << this->mSurface; + if (this->isInitialized()) this->destroy(); +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_active() { + qCDebug(logShortcutsInhibit) << "Inhibitor became active" << this; + this->mActive = true; + emit this->activeChanged(); +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_inactive() { + qCDebug(logShortcutsInhibit) << "Inhibitor became inactive" << this; + this->mActive = false; + emit this->activeChanged(); +} + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/proto.hpp b/src/wayland/shortcuts_inhibit/proto.hpp new file mode 100644 index 00000000..d19b579e --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::shortcuts_inhibit::impl { + +class ShortcutsInhibitor; + +class ShortcutsInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1 { +public: + explicit ShortcutsInhibitManager(); + + ShortcutsInhibitor* createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface); + void refShortcutsInhibitor(ShortcutsInhibitor* inhibitor); + void unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor); + + static ShortcutsInhibitManager* instance(); + +private: + QHash inhibitors; + QHash refCounts; +}; + +class ShortcutsInhibitor + : public QObject + , public QtWayland::zwp_keyboard_shortcuts_inhibitor_v1 { + Q_OBJECT; + +public: + explicit ShortcutsInhibitor(::zwp_keyboard_shortcuts_inhibitor_v1* inhibitor, wl_surface* surface) + : QtWayland::zwp_keyboard_shortcuts_inhibitor_v1(inhibitor) + , mSurface(surface) {} + + ~ShortcutsInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutsInhibitor); + + [[nodiscard]] bool isActive() const { return this->mActive; } + [[nodiscard]] wl_surface* surface() const { return this->mSurface; } + +signals: + void activeChanged(); + +protected: + void zwp_keyboard_shortcuts_inhibitor_v1_active() override; + void zwp_keyboard_shortcuts_inhibitor_v1_inactive() override; + +private: + bool mActive = false; + wl_surface* mSurface; +}; + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file