From bba8cb8a7dcce95905b6dc3c23d1b1c8e56cab2c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 May 2024 22:05:46 -0700 Subject: [PATCH] hyprland/global_shortcuts: add GlobalShortcut --- CMakeLists.txt | 6 + src/services/CMakeLists.txt | 4 +- src/wayland/hyprland/CMakeLists.txt | 8 +- .../hyprland/focus_grab/CMakeLists.txt | 16 ++- .../hyprland/global_shortcuts/CMakeLists.txt | 29 +++++ .../hyprland-global-shortcuts-v1.xml | 112 +++++++++++++++++ .../hyprland/global_shortcuts/init.cpp | 20 +++ .../hyprland/global_shortcuts/manager.cpp | 55 +++++++++ .../hyprland/global_shortcuts/manager.hpp | 34 ++++++ src/wayland/hyprland/global_shortcuts/qml.cpp | 115 ++++++++++++++++++ src/wayland/hyprland/global_shortcuts/qml.hpp | 108 ++++++++++++++++ .../hyprland/global_shortcuts/shortcut.cpp | 33 +++++ .../hyprland/global_shortcuts/shortcut.hpp | 32 +++++ src/wayland/hyprland/module.md | 3 +- 14 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 src/wayland/hyprland/global_shortcuts/CMakeLists.txt create mode 100644 src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml create mode 100644 src/wayland/hyprland/global_shortcuts/init.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/manager.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/manager.hpp create mode 100644 src/wayland/hyprland/global_shortcuts/qml.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/qml.hpp create mode 100644 src/wayland/hyprland/global_shortcuts/shortcut.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/shortcut.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f2042f..7eb81f5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) +option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) message(STATUS "Quickshell configuration") @@ -29,6 +31,10 @@ endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " Hyprland: ${HYPRLAND}") +if (HYPRLAND) + message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") + message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") +endif() if (NOT DEFINED GIT_REVISION) execute_process( diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 909acc00..56d7f669 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -1 +1,3 @@ -add_subdirectory(status_notifier) +if (SERVICE_STATUS_NOTIFIER) + add_subdirectory(status_notifier) +endif() diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 4bc0eeaa..06121a7e 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -1,7 +1,13 @@ qt_add_library(quickshell-hyprland STATIC) qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) -add_subdirectory(focus_grab) +if (HYPRLAND_FOCUS_GRAB) + add_subdirectory(focus_grab) +endif() + +if (HYPRLAND_GLOBAL_SHORTCUTS) + add_subdirectory(global_shortcuts) +endif() target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 7e826aa5..587ae939 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -4,11 +4,18 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC qml.cpp ) -qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1) +qt_add_qml_module(quickshell-hyprland-focus-grab + URI Quickshell.Hyprland._FocusGrab + VERSION 0.1 +) add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) -wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml") +wl_proto(quickshell-hyprland-focus-grab + hyprland-focus-grab-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" +) + target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) @@ -16,4 +23,7 @@ qs_pch(quickshell-hyprland-focus-grab) qs_pch(quickshell-hyprland-focus-grabplugin) qs_pch(quickshell-hyprland-focus-grab-init) -target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init) +target_link_libraries(quickshell PRIVATE + quickshell-hyprland-focus-grabplugin + quickshell-hyprland-focus-grab-init +) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt new file mode 100644 index 00000000..804c0a3c --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -0,0 +1,29 @@ +qt_add_library(quickshell-hyprland-global-shortcuts STATIC + qml.cpp + manager.cpp + shortcut.cpp +) + +qt_add_qml_module(quickshell-hyprland-global-shortcuts + URI Quickshell.Hyprland._GlobalShortcuts + VERSION 0.1 +) + +add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp) + +wl_proto(quickshell-hyprland-global-shortcuts + hyprland-global-shortcuts-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" +) + +target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-global-shortcuts) +qs_pch(quickshell-hyprland-global-shortcutsplugin) +qs_pch(quickshell-hyprland-global-shortcuts-init) + +target_link_libraries(quickshell PRIVATE + quickshell-hyprland-global-shortcutsplugin + quickshell-hyprland-global-shortcuts-init +) diff --git a/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml new file mode 100644 index 00000000..784d887e --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml @@ -0,0 +1,112 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows a client to register triggerable actions, + meant to be global shortcuts. + + + + + This object is a manager which offers requests to create global shortcuts. + + + + + Register a new global shortcut. + + A global shortcut is anonymous, meaning the app does not know what key(s) trigger it. + + The shortcut's keybinding shall be dealt with by the compositor. + + In the case of a duplicate app_id + id combination, the already_taken protocol error is raised. + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + + + + + This object represents a single shortcut. + + + + + The keystroke was pressed. + + tv_ values hold the timestamp of the occurrence. + + + + + + + + + The keystroke was released. + + tv_ values hold the timestamp of the occurrence. + + + + + + + + + Destroys the shortcut. Can be sent at any time by the client. + + + + diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp new file mode 100644 index 00000000..12fed07f --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/init.cpp @@ -0,0 +1,20 @@ +#include + +#include "../../../core/plugin.hpp" + +namespace { + +class HyprlandFocusGrabPlugin: public QuickshellPlugin { + void registerTypes() override { + qmlRegisterModuleImport( + "Quickshell.Hyprland", + QQmlModuleImportModuleAny, + "Quickshell.Hyprland._GlobalShortcuts", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); + +} // namespace diff --git a/src/wayland/hyprland/global_shortcuts/manager.cpp b/src/wayland/hyprland/global_shortcuts/manager.cpp new file mode 100644 index 00000000..ce0f24c4 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/manager.cpp @@ -0,0 +1,55 @@ +#include "manager.hpp" + +#include +#include + +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts::impl { + +GlobalShortcutManager::GlobalShortcutManager() + : QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +GlobalShortcut* GlobalShortcutManager::registerShortcut( + const QString& appid, + const QString& name, + const QString& description, + const QString& triggerDescription +) { + auto shortcut = this->shortcuts.value({appid, name}); + + if (shortcut.second != nullptr) { + this->shortcuts.insert({appid, name}, {shortcut.first + 1, shortcut.second}); + return shortcut.second; + } else { + auto* shortcutObj = this->register_shortcut(name, appid, description, triggerDescription); + auto* managedObj = new GlobalShortcut(shortcutObj); + this->shortcuts.insert({appid, name}, {1, managedObj}); + return managedObj; + } +} + +void GlobalShortcutManager::unregisterShortcut(const QString& appid, const QString& name) { + auto shortcut = this->shortcuts.value({appid, name}); + + if (shortcut.first > 1) { + this->shortcuts.insert({appid, name}, {shortcut.first - 1, shortcut.second}); + } else { + delete shortcut.second; + this->shortcuts.remove({appid, name}); + } +} + +GlobalShortcutManager* GlobalShortcutManager::instance() { + static GlobalShortcutManager* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new GlobalShortcutManager(); + } + + return instance; +} + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/manager.hpp b/src/wayland/hyprland/global_shortcuts/manager.hpp new file mode 100644 index 00000000..0a165c53 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/manager.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts::impl { + +class GlobalShortcutManager + : public QWaylandClientExtensionTemplate + , public QtWayland::hyprland_global_shortcuts_manager_v1 { +public: + explicit GlobalShortcutManager(); + + GlobalShortcut* registerShortcut( + const QString& appid, + const QString& name, + const QString& description, + const QString& triggerDescription + ); + + void unregisterShortcut(const QString& appid, const QString& name); + + static GlobalShortcutManager* instance(); + +private: + QHash, QPair> shortcuts; +}; + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/qml.cpp b/src/wayland/hyprland/global_shortcuts/qml.cpp new file mode 100644 index 00000000..ff957eaf --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/qml.cpp @@ -0,0 +1,115 @@ +#include "qml.hpp" +#include + +#include +#include +#include + +#include "manager.hpp" +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts { +using impl::GlobalShortcutManager; + +GlobalShortcut::~GlobalShortcut() { + auto* manager = GlobalShortcutManager::instance(); + if (manager != nullptr) { + manager->unregisterShortcut(this->mAppid, this->mName); + } +} + +void GlobalShortcut::onPostReload() { + if (this->mName.isEmpty()) { + qWarning() << "Unable to create GlobalShortcut with empty name."; + return; + } + + auto* manager = GlobalShortcutManager::instance(); + if (manager == nullptr) { + qWarning() << "The active compositor does not support hyprland_global_shortcuts_v1."; + qWarning() << "GlobalShortcut will not work."; + return; + } + + this->shortcut = manager->registerShortcut( + this->mAppid, + this->mName, + this->mDescription, + this->mTriggerDescription + ); + + QObject::connect(this->shortcut, &ShortcutImpl::pressed, this, &GlobalShortcut::onPressed); + QObject::connect(this->shortcut, &ShortcutImpl::released, this, &GlobalShortcut::onReleased); +} + +bool GlobalShortcut::isPressed() const { return this->mPressed; } + +QString GlobalShortcut::appid() const { return this->mAppid; } + +void GlobalShortcut::setAppid(QString appid) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (appid == this->mAppid) return; + + this->mAppid = std::move(appid); + emit this->appidChanged(); +} + +QString GlobalShortcut::name() const { return this->mName; } + +void GlobalShortcut::setName(QString name) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (name == this->mName) return; + + this->mName = std::move(name); + emit this->nameChanged(); +} + +QString GlobalShortcut::description() const { return this->mDescription; } + +void GlobalShortcut::setDescription(QString description) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (description == this->mDescription) return; + + this->mDescription = std::move(description); + emit this->descriptionChanged(); +} + +QString GlobalShortcut::triggerDescription() const { return this->mTriggerDescription; } + +void GlobalShortcut::setTriggerDescription(QString triggerDescription) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (triggerDescription == this->mTriggerDescription) return; + + this->mTriggerDescription = std::move(triggerDescription); + emit this->triggerDescriptionChanged(); +} + +void GlobalShortcut::onPressed() { + this->mPressed = true; + emit this->pressed(); + emit this->pressedChanged(); +} + +void GlobalShortcut::onReleased() { + this->mPressed = false; + emit this->released(); + emit this->pressedChanged(); +} + +} // namespace qs::hyprland::global_shortcuts diff --git a/src/wayland/hyprland/global_shortcuts/qml.hpp b/src/wayland/hyprland/global_shortcuts/qml.hpp new file mode 100644 index 00000000..a43d963e --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/qml.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/reload.hpp" +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts { + +///! Hyprland global shortcut. +/// Global shortcut implemented with [hyprland_global_shortcuts_v1]. +/// +/// You can use this within hyprland as a global shortcut: +/// ``` +/// bind = , , global, : +/// ``` +/// See [the wiki] for details. +/// +/// > [!WARNING] The shortcuts protocol does not allow duplicate appid + name pairs. +/// > Within a single instance of quickshell this is handled internally, and both +/// > users will be notified, but multiple instances of quickshell or XDPH may collide. +/// > +/// > If that happens, whichever client that tries to register the shortcuts last will crash. +/// +/// > [!INFO] This type does *not* use the xdg-desktop-portal global shortcuts protocol, +/// > as it is not fully functional without flatpak and would cause a considerably worse +/// > user experience from other limitations. It will only work with Hyprland. +/// > Note that, as this type bypasses xdg-desktop-portal, XDPH is not required. +/// +/// [hyprland_global_shortcuts_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml +/// [the wiki]: https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts +class GlobalShortcut + : public QObject + , public PostReloadHook { + using ShortcutImpl = impl::GlobalShortcut; + + Q_OBJECT; + // clang-format off + /// If the keybind is currently pressed. + Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged); + /// The appid of the shortcut. Defaults to `quickshell`. + /// You cannot change this at runtime. + /// + /// If you have more than one shortcut we recommend subclassing + /// GlobalShortcut to set this. + Q_PROPERTY(QString appid READ appid WRITE setAppid NOTIFY appidChanged); + /// The name of the shortcut. + /// You cannot change this at runtime. + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged); + /// The description of the shortcut that appears in `hyprctl globalshortcuts`. + /// You cannot change this at runtime. + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged); + /// Have not seen this used ever, but included for completeness. Safe to ignore. + Q_PROPERTY(QString triggerDescription READ triggerDescription WRITE setTriggerDescription NOTIFY triggerDescriptionChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit GlobalShortcut(QObject* parent = nullptr): QObject(parent) {} + ~GlobalShortcut() override; + Q_DISABLE_COPY_MOVE(GlobalShortcut); + + void onPostReload() override; + + [[nodiscard]] bool isPressed() const; + + [[nodiscard]] QString appid() const; + void setAppid(QString appid); + + [[nodiscard]] QString name() const; + void setName(QString name); + + [[nodiscard]] QString description() const; + void setDescription(QString description); + + [[nodiscard]] QString triggerDescription() const; + void setTriggerDescription(QString triggerDescription); + +signals: + /// Fired when the keybind is pressed. + void pressed(); + /// Fired when the keybind is released. + void released(); + + void pressedChanged(); + void appidChanged(); + void nameChanged(); + void descriptionChanged(); + void triggerDescriptionChanged(); + +private slots: + void onPressed(); + void onReleased(); + +private: + impl::GlobalShortcut* shortcut = nullptr; + + bool mPressed = false; + QString mAppid = "quickshell"; + QString mName; + QString mDescription; + QString mTriggerDescription; +}; + +} // namespace qs::hyprland::global_shortcuts diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.cpp b/src/wayland/hyprland/global_shortcuts/shortcut.cpp new file mode 100644 index 00000000..2178d775 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/shortcut.cpp @@ -0,0 +1,33 @@ +#include "shortcut.hpp" + +#include +#include +#include + +namespace qs::hyprland::global_shortcuts::impl { + +GlobalShortcut::GlobalShortcut(::hyprland_global_shortcut_v1* shortcut) { this->init(shortcut); } + +GlobalShortcut::~GlobalShortcut() { + if (this->isInitialized()) { + this->destroy(); + } +} + +void GlobalShortcut::hyprland_global_shortcut_v1_pressed( + quint32 /*unused*/, + quint32 /*unused*/, + quint32 /*unused*/ +) { + emit this->pressed(); +} + +void GlobalShortcut::hyprland_global_shortcut_v1_released( + quint32 /*unused*/, + quint32 /*unused*/, + quint32 /*unused*/ +) { + emit this->released(); +} + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.hpp b/src/wayland/hyprland/global_shortcuts/shortcut.hpp new file mode 100644 index 00000000..6aa8fd58 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/shortcut.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::hyprland::global_shortcuts::impl { + +class GlobalShortcut + : public QObject + , public QtWayland::hyprland_global_shortcut_v1 { + Q_OBJECT; + +public: + explicit GlobalShortcut(::hyprland_global_shortcut_v1* shortcut); + ~GlobalShortcut() override; + Q_DISABLE_COPY_MOVE(GlobalShortcut); + +signals: + void pressed(); + void released(); + +private: + // clang-format off + void hyprland_global_shortcut_v1_pressed(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override; + void hyprland_global_shortcut_v1_released(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override; + // clang-format on +}; + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index f00bce37..1b3e2fbf 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,7 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ - "focus_grab/qml.hpp" + "focus_grab/qml.hpp", + "global_shortcuts/qml.hpp", ] -----