diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cb376d..e5f2042 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) 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(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) message(STATUS "Quickshell configuration") @@ -27,6 +28,7 @@ if (WAYLAND) endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") +message(STATUS " Hyprland: ${HYPRLAND}") if (NOT DEFINED GIT_REVISION) execute_process( diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 17473dc..e2a80a5 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -88,6 +88,7 @@ void ProxyWindowBase::createWindow() { } void ProxyWindowBase::deleteWindow() { + if (this->window != nullptr) emit this->windowDestroyed(); if (auto* window = this->disownWindow()) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp index 14ee09d..40f14c4 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -102,6 +102,7 @@ public: signals: void windowConnected(); + void windowDestroyed(); void visibleChanged(); void backerVisibilityChanged(); void xChanged(); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index e56c043..48140a9 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -68,6 +68,10 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (HYPRLAND) + add_subdirectory(hyprland) +endif() + target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt new file mode 100644 index 0000000..4bc0eea --- /dev/null +++ b/src/wayland/hyprland/CMakeLists.txt @@ -0,0 +1,11 @@ +qt_add_library(quickshell-hyprland STATIC) +qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +add_subdirectory(focus_grab) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland) +qs_pch(quickshell-hyprlandplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt new file mode 100644 index 0000000..7e826aa --- /dev/null +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-hyprland-focus-grab STATIC + manager.cpp + grab.cpp + qml.cpp +) + +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") +target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) + +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) diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp new file mode 100644 index 0000000..a45cf4e --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -0,0 +1,78 @@ +#include "grab.hpp" + +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { + +FocusGrab::FocusGrab(::hyprland_focus_grab_v1* grab) { this->init(grab); } + +FocusGrab::~FocusGrab() { + if (this->isInitialized()) { + this->destroy(); + } +} + +bool FocusGrab::isActive() const { return this->active; } + +void FocusGrab::addWindow(QWindow* window) { + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->addWaylandWindow(waylandWindow); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->addWaylandWindow(waylandWindow); + this->sync(); + } + }); + } +} + +void FocusGrab::removeWindow(QWindow* window) { + QObject::disconnect(window, nullptr, this, nullptr); + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->pendingAdditions.removeAll(waylandWindow); + this->remove_surface(waylandWindow->surface()); + this->commitRequired = true; + } +} + +void FocusGrab::addWaylandWindow(QWaylandWindow* window) { + this->add_surface(window->surface()); + this->pendingAdditions.append(window); + this->commitRequired = true; +} + +void FocusGrab::sync() { + if (this->commitRequired) { + this->commit(); + this->commitRequired = false; + + // the protocol will always send cleared() when the grab is deactivated, + // even if it was due to window destruction, so we don't need to track it. + if (!this->pendingAdditions.isEmpty()) { + this->pendingAdditions.clear(); + + if (!this->active) { + this->active = true; + emit this->activated(); + } + } + } +} + +void FocusGrab::hyprland_focus_grab_v1_cleared() { + this->active = false; + emit this->cleared(); +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/grab.hpp b/src/wayland/hyprland/focus_grab/grab.hpp new file mode 100644 index 0000000..2a9384d --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrab = QtWayland::hyprland_focus_grab_v1; + +class FocusGrab + : public QObject + , public HyprlandFocusGrab { + using QWaylandWindow = QtWaylandClient::QWaylandWindow; + + Q_OBJECT; + +public: + explicit FocusGrab(::hyprland_focus_grab_v1* grab); + ~FocusGrab() override; + Q_DISABLE_COPY_MOVE(FocusGrab); + + [[nodiscard]] bool isActive() const; + void addWindow(QWindow* window); + void removeWindow(QWindow* window); + void sync(); + +signals: + void activated(); + void cleared(); + +private: + void hyprland_focus_grab_v1_cleared() override; + + void addWaylandWindow(QWaylandWindow* window); + + QList pendingAdditions; + bool commitRequired = false; + bool active = false; +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml new file mode 100644 index 0000000..3b3cd34 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml @@ -0,0 +1,128 @@ + + + + Copyright © 2024 outfoxxed + 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 clients to limit input focus to a specific set + of surfaces and receive a notification when the limiter is removed as + detailed below. + + + + + This interface allows a client to create surface grab objects. + + + + + Create a surface grab object. + + + + + + + + Destroy the focus grab manager. + This doesn't destroy existing focus grab objects. + + + + + + + This interface restricts input focus to a specified whitelist of + surfaces as long as the focus grab object exists and has at least + one comitted surface. + + Mouse and touch events inside a whitelisted surface will be passed + to the surface normally, while events outside of a whitelisted surface + will clear the grab object. Keyboard events will be passed to the client + and a compositor-picked surface in the whitelist will receive a + wl_keyboard::enter event if a whitelisted surface is not already entered. + + Upon meeting implementation-defined criteria usually meaning a mouse or + touch input outside of any whitelisted surfaces, the compositor will + clear the whitelist, rendering the grab inert and sending the cleared + event. The same will happen if another focus grab or similar action + is started at the compositor's discretion. + + + + + Add a surface to the whitelist. Destroying the surface is treated the + same as an explicit call to remove_surface and duplicate additions are + ignored. + + Does not take effect until commit is called. + + + + + + + + Remove a surface from the whitelist. Destroying the surface is treated + the same as an explicit call to this function. + + If the grab was active and the removed surface was entered by the + keyboard, another surface will be entered on commit. + + Does not take effect until commit is called. + + + + + + + + Commit pending changes to the surface whitelist. + + If the list previously had no entries and now has at least one, the grab + will start. If it previously had entries and now has none, the grab will + become inert. + + + + + + Destroy the grab object and remove the grab if active. + + + + + + Sent when an active grab is cancelled by the compositor, + regardless of cause. + + + + diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp new file mode 100644 index 0000000..784c7f2 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/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._FocusGrab", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); + +} // namespace diff --git a/src/wayland/hyprland/focus_grab/manager.cpp b/src/wayland/hyprland/focus_grab/manager.cpp new file mode 100644 index 0000000..fb93e31 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.cpp @@ -0,0 +1,27 @@ +#include "manager.hpp" + +#include + +#include "grab.hpp" + +namespace qs::hyprland::focus_grab { + +FocusGrabManager::FocusGrabManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +bool FocusGrabManager::available() const { return this->isActive(); } + +FocusGrab* FocusGrabManager::createGrab() { return new FocusGrab(this->create_grab()); } + +FocusGrabManager* FocusGrabManager::instance() { + static FocusGrabManager* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new FocusGrabManager(); + } + + return instance; +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/manager.hpp b/src/wayland/hyprland/focus_grab/manager.hpp new file mode 100644 index 0000000..8668148 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrabManager = QtWayland::hyprland_focus_grab_manager_v1; +class FocusGrab; + +class FocusGrabManager + : public QWaylandClientExtensionTemplate + , public HyprlandFocusGrabManager { +public: + explicit FocusGrabManager(); + + [[nodiscard]] bool available() const; + [[nodiscard]] FocusGrab* createGrab(); + + static FocusGrabManager* instance(); +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp new file mode 100644 index 0000000..ca644b9 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -0,0 +1,131 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../../core/proxywindow.hpp" +#include "../../../core/windowinterface.hpp" +#include "grab.hpp" +#include "manager.hpp" + +namespace qs::hyprland { +using focus_grab::FocusGrab; +using focus_grab::FocusGrabManager; + +void HyprlandFocusGrab::componentComplete() { this->tryActivate(); } + +bool HyprlandFocusGrab::isActive() const { return this->grab != nullptr && this->grab->isActive(); } + +void HyprlandFocusGrab::setActive(bool active) { + if (active == this->targetActive) return; + this->targetActive = active; + + if (!active) { + delete this->grab; + this->grab = nullptr; + emit this->activeChanged(); + } else { + this->tryActivate(); + } +} + +QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; } + +void HyprlandFocusGrab::setWindows(QObjectList windows) { + if (windows == this->windowObjects) return; + this->windowObjects = std::move(windows); + this->syncWindows(); + emit this->windowsChanged(); +} + +void HyprlandFocusGrab::onGrabActivated() { emit this->activeChanged(); } + +void HyprlandFocusGrab::onGrabCleared() { + emit this->cleared(); + this->setActive(false); +} + +void HyprlandFocusGrab::onProxyConnected() { + if (this->grab != nullptr) { + this->grab->addWindow(qobject_cast(this->sender())->backingWindow()); + this->grab->sync(); + } +} + +void HyprlandFocusGrab::tryActivate() { + if (!this->targetActive || this->isActive()) return; + + auto* manager = FocusGrabManager::instance(); + if (!manager->isActive()) { + qWarning() << "The active compositor does not support the hyprland_focus_grab_v1 protocol. " + "HyprlandFocusGrab will not work."; + qWarning() << "** Learn why $XDG_CURRENT_DESKTOP sucks and download a better compositor " + "today at https://hyprland.org"; + return; + } + + this->grab = manager->createGrab(); + this->grab->setParent(this); + QObject::connect(this->grab, &FocusGrab::activated, this, &HyprlandFocusGrab::onGrabActivated); + QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); + + for (auto* proxy: this->trackedProxies) { + if (proxy->backingWindow() != nullptr) { + this->grab->addWindow(proxy->backingWindow()); + } + } + + this->grab->sync(); +} + +void HyprlandFocusGrab::syncWindows() { + auto newProxy = QList(); + for (auto* windowObject: this->windowObjects) { + auto* proxyWindow = qobject_cast(windowObject); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(windowObject)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != nullptr) { + newProxy.push_back(proxyWindow); + } + } + + for (auto* oldWindow: this->trackedProxies) { + if (!newProxy.contains(oldWindow)) { + QObject::disconnect(oldWindow, nullptr, this, nullptr); + + if (this->grab != nullptr && oldWindow->backingWindow() != nullptr) { + this->grab->removeWindow(oldWindow->backingWindow()); + } + } + } + + for (auto* newProxy: newProxy) { + if (!this->trackedProxies.contains(newProxy)) { + QObject::connect( + newProxy, + &ProxyWindowBase::windowConnected, + this, + &HyprlandFocusGrab::onProxyConnected + ); + + if (this->grab != nullptr && newProxy->backingWindow() != nullptr) { + this->grab->addWindow(newProxy->backingWindow()); + } + } + } + + this->trackedProxies = newProxy; + if (this->grab != nullptr) this->grab->sync(); +} + +} // namespace qs::hyprland diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp new file mode 100644 index 0000000..4ba7227 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class ProxyWindowBase; + +namespace qs::hyprland { + +namespace focus_grab { +class FocusGrab; +} + +///! Input focus grabber +/// Object for managing input focus grabs via the [hyprland_focus_grab_v1] +/// wayland protocol. +/// +/// When enabled, all of the windows listed in the `windows` property will +/// receive input normally, and will retain keyboard focus even if the mouse +/// is moved off of them. When areas of the screen that are not part of a listed +/// window are clicked or touched, the grab will become inactive and emit the +/// cleared signal. +/// +/// This is useful for implementing dismissal of popup type windows. +/// ```qml +/// import Quickshell +/// import Quickshell.Hyprland +/// import QtQuick.Controls +/// +/// ShellRoot { +/// FloatingWindow { +/// id: window +/// +/// Button { +/// anchors.centerIn: parent +/// text: grab.active ? "Remove exclusive focus" : "Take exclusive focus" +/// onClicked: grab.active = !grab.active +/// } +/// +/// HyprlandFocusGrab { +/// id: grab +/// windows: [ window ] +/// } +/// } +/// } +/// ``` +/// +/// [hyprland_focus_grab_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml +class HyprlandFocusGrab + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + /// If the focus grab is active. Defaults to false. + /// + /// When set to true, an input grab will be created for the listed windows. + /// + /// This property will change to false once the grab is dismissed. + /// It will not change to true until the grab begins, which requires + /// at least one visible window. + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); + /// The list of windows to whitelist for input. + Q_PROPERTY(QList windows READ windows WRITE setWindows NOTIFY windowsChanged); + QML_ELEMENT; + +public: + explicit HyprlandFocusGrab(QObject* parent = nullptr): QObject(parent) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] QObjectList windows() const; + void setWindows(QObjectList windows); + +signals: + /// Sent whenever the compositor clears the focus grab. + /// + /// This may be in response to all windows being removed + /// from the list or simultaneously hidden, in addition to + /// a normal clear. + void cleared(); + + void activeChanged(); + void windowsChanged(); + +private slots: + void onGrabActivated(); + void onGrabCleared(); + void onProxyConnected(); + +private: + void tryActivate(); + void syncWindows(); + + bool targetActive = false; + QObjectList windowObjects; + QList trackedProxies; + QList trackedWindows; + + focus_grab::FocusGrab* grab = nullptr; +}; + +}; // namespace qs::hyprland diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md new file mode 100644 index 0000000..f00bce3 --- /dev/null +++ b/src/wayland/hyprland/module.md @@ -0,0 +1,6 @@ +name = "Quickshell.Hyprland" +description = "Hyprland specific Quickshell types" +headers = [ + "focus_grab/qml.hpp" +] +-----