From ed036d514b0fdbce03158a0b331305be166f4555 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 29 Sep 2025 18:20:04 -0400 Subject: [PATCH] wayland/shortcuts-inhibit: add shortcuts inhibitor --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 3 + src/wayland/module.md | 1 + src/wayland/shortcuts_inhibit/CMakeLists.txt | 25 +++ src/wayland/shortcuts_inhibit/inhibitor.cpp | 187 ++++++++++++++++++ src/wayland/shortcuts_inhibit/inhibitor.hpp | 89 +++++++++ src/wayland/shortcuts_inhibit/proto.cpp | 88 +++++++++ src/wayland/shortcuts_inhibit/proto.hpp | 64 ++++++ .../shortcuts_inhibit/test/manual/test.qml | 65 ++++++ 9 files changed, 523 insertions(+) create mode 100644 src/wayland/shortcuts_inhibit/CMakeLists.txt create mode 100644 src/wayland/shortcuts_inhibit/inhibitor.cpp create mode 100644 src/wayland/shortcuts_inhibit/inhibitor.hpp create mode 100644 src/wayland/shortcuts_inhibit/proto.cpp create mode 100644 src/wayland/shortcuts_inhibit/proto.hpp create mode 100644 src/wayland/shortcuts_inhibit/test/manual/test.qml diff --git a/changelog/next.md b/changelog/next.md index 4b255ff..b03a52b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -14,6 +14,7 @@ set shell id. - Added support for creating Polkit agents. - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. +- Added support for inhibiting wayland compositor shortcuts for focused windows. - Added the ability to override Quickshell.cacheDir with a custom path. ## Other Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index a96fe6b..ca49c8f 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/module.md b/src/wayland/module.md index 0216e6d..9ad15ba 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -7,5 +7,6 @@ headers = [ "screencopy/view.hpp", "idle_inhibit/inhibitor.hpp", "idle_notify/monitor.hpp", + "shortcuts_inhibit/inhibitor.hpp", ] ----- diff --git a/src/wayland/shortcuts_inhibit/CMakeLists.txt b/src/wayland/shortcuts_inhibit/CMakeLists.txt new file mode 100644 index 0000000..8dedd3d --- /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 0000000..2fca9bc --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -0,0 +1,187 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { +using QtWaylandClient::QWaylandWindow; + +ShortcutInhibitor::ShortcutInhibitor() { + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); + + this->bActive.setBinding([this]() { + auto* inhibitor = this->bInhibitor.value(); + if (!inhibitor) return false; + if (!inhibitor->bindableActive().value()) return false; + return this->bWindow.value() == this->bFocusedWindow; + }); + + QObject::connect( + dynamic_cast(QGuiApplication::instance()), + &QGuiApplication::focusWindowChanged, + this, + &ShortcutInhibitor::onFocusedWindowChanged + ); + + this->onFocusedWindowChanged(QGuiApplication::focusWindow()); +} + +ShortcutInhibitor::~ShortcutInhibitor() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); +} + +void ShortcutInhibitor::onBoundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (!proxyWindow) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect(proxyWindow, &QObject::destroyed, this, &ShortcutInhibitor::onWindowDestroyed); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &ShortcutInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } +} + +void ShortcutInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); +} + +void ShortcutInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (!waylandWindow) { + qCCritical(impl::logShortcutsInhibit()) << "Window handle is not a QWaylandWindow"; + return; + } + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + this->bWindow = window; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &ShortcutInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &ShortcutInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &ShortcutInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void ShortcutInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void ShortcutInhibitor::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->bInhibitor) { + qFatal("ShortcutsInhibitor: inhibitor already exists when creating surface"); + } + + this->bInhibitor = manager->createShortcutsInhibitor(this->mWaylandWindow); +} + +void ShortcutInhibitor::onWaylandSurfaceDestroyed() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); + this->bInhibitor = nullptr; +} + +void ShortcutInhibitor::onInhibitorChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor) { + QObject::connect( + inhibitor, + &impl::ShortcutsInhibitor::activeChanged, + this, + &ShortcutInhibitor::onInhibitorActiveChanged + ); + } +} + +void ShortcutInhibitor::onInhibitorActiveChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor && !inhibitor->isActive()) { + // Compositor has deactivated the inhibitor, making it invalid. + // Set enabled to false so the user can enable it again to create a new inhibitor. + this->bEnabled = false; + emit this->cancelled(); + } +} + +void ShortcutInhibitor::onFocusedWindowChanged(QWindow* focusedWindow) { + this->bFocusedWindow = focusedWindow; +} + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/inhibitor.hpp b/src/wayland/shortcuts_inhibit/inhibitor.hpp new file mode 100644 index 0000000..2eed54f --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#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 ShortcutInhibitor: 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. + /// The inhibitor will only inhibit shortcuts pressed while this window has keyboard focus. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ default WRITE default NOTIFY windowChanged BINDABLE bindableWindow); + /// Whether the inhibitor is currently active. The inhibitor is only active if @@enabled is true, + /// @@window has keyboard focus, and the compositor grants the inhibit request. + /// + /// The compositor may deactivate the inhibitor at any time (for example, if the user requests + /// normal shortcuts to be restored). When deactivated by the compositor, the inhibitor cannot be + /// programmatically reactivated. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + // clang-format on + +public: + ShortcutInhibitor(); + ~ShortcutInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutInhibitor); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + [[nodiscard]] QBindable bindableWindow() { return &this->bWindowObject; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + +signals: + void enabledChanged(); + void windowChanged(); + void activeChanged(); + /// Sent if the compositor cancels the inhibitor while it is active. + void cancelled(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onInhibitorActiveChanged(); + +private: + void onBoundWindowChanged(); + void onInhibitorChanged(); + void onFocusedWindowChanged(QWindow* focusedWindow); + + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bEnabled, &ShortcutInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bWindowObject, &ShortcutInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bBoundWindow, &ShortcutInhibitor::onBoundWindowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, impl::ShortcutsInhibitor*, bInhibitor, &ShortcutInhibitor::onInhibitorChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bFocusedWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bActive, &ShortcutInhibitor::activeChanged); + // clang-format on +}; + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/proto.cpp b/src/wayland/shortcuts_inhibit/proto.cpp new file mode 100644 index 0000000..8d35d5e --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.cpp @@ -0,0 +1,88 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::shortcuts_inhibit::impl { + +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& pair = this->inhibitors[wlSurface]; + pair.second++; + qCDebug(logShortcutsInhibit) << "Reusing existing inhibitor" << pair.first << "for surface" + << wlSurface << "- refcount:" << pair.second; + return pair.first; + } + + auto* inhibitor = + new ShortcutsInhibitor(this->inhibit_shortcuts(wlSurface, inputDevice->object()), wlSurface); + this->inhibitors.insert(wlSurface, qMakePair(inhibitor, 1)); + qCDebug(logShortcutsInhibit) << "Created inhibitor" << inhibitor << "for surface" << wlSurface; + return inhibitor; +} + +void ShortcutsInhibitManager::unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor) { + if (!inhibitor) return; + + auto* surface = inhibitor->surface(); + if (!this->inhibitors.contains(surface)) return; + + auto& pair = this->inhibitors[surface]; + pair.second--; + qCDebug(logShortcutsInhibit) << "Decremented refcount for inhibitor" << inhibitor + << "- refcount:" << pair.second; + + if (pair.second <= 0) { + qCDebug(logShortcutsInhibit) << "Refcount reached 0, destroying inhibitor" << inhibitor; + this->inhibitors.remove(surface); + 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->bActive = true; +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_inactive() { + qCDebug(logShortcutsInhibit) << "Inhibitor became inactive" << this; + this->bActive = false; +} + +} // 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 0000000..e79d5ca --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::shortcuts_inhibit::impl { + +QS_DECLARE_LOGGING_CATEGORY(logShortcutsInhibit); + +class ShortcutsInhibitor; + +class ShortcutsInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1 { +public: + explicit ShortcutsInhibitManager(); + + ShortcutsInhibitor* createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface); + void unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor); + + static ShortcutsInhibitManager* instance(); + +private: + QHash> inhibitors; +}; + +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) + , bActive(false) {} + + ~ShortcutsInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutsInhibitor); + + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] bool isActive() const { return this->bActive; } + [[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: + wl_surface* mSurface; + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, bool, bActive, &ShortcutsInhibitor::activeChanged); +}; + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/test/manual/test.qml b/src/wayland/shortcuts_inhibit/test/manual/test.qml new file mode 100644 index 0000000..1f64cbf --- /dev/null +++ b/src/wayland/shortcuts_inhibit/test/manual/test.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +Scope { + Timer { + id: toggleTimer + interval: 100 + onTriggered: windowLoader.active = true + } + + LazyLoader { + id: windowLoader + active: true + + property bool enabled: false + + FloatingWindow { + id: w + color: contentItem.palette.window + + ColumnLayout { + anchors.centerIn: parent + + CheckBox { + id: loadedCb + text: "Loaded" + checked: true + } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: windowLoader.enabled + onCheckedChanged: windowLoader.enabled = checked + } + + Label { + text: `Active: ${inhibitorLoader.item?.active ?? false}` + } + + Button { + text: "Toggle Window" + onClicked: { + windowLoader.active = false; + toggleTimer.start(); + } + } + } + + LazyLoader { + id: inhibitorLoader + active: loadedCb.checked + + ShortcutInhibitor { + window: w + enabled: enabledCb.checked + onCancelled: enabledCb.checked = false + } + } + } + } +}