diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index fd1da674..fd014635 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -17,6 +17,21 @@ install_qml_module(quickshell-hyprland-ipc) target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) +if (WAYLAND_TOPLEVEL_MANAGEMENT) + target_sources(quickshell-hyprland-ipc PRIVATE + toplevel_mapping.cpp + hyprland_toplevel.cpp + ) + + wl_proto(wlp-hyprland-toplevel-mapping hyprland-toplevel-mapping-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + + target_link_libraries(quickshell-hyprland-ipc PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-hyprland-toplevel-mapping + wlp-foreign-toplevel + ) +endif() + qs_module_pch(quickshell-hyprland-ipc SET large) target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml b/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml new file mode 100644 index 00000000..ec7b1cea --- /dev/null +++ b/src/wayland/hyprland/ipc/hyprland-toplevel-mapping-v1.xml @@ -0,0 +1,113 @@ + + + + Copyright © 2025 WhySoBad + 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 retrieve the mapping of toplevels to hyprland window addresses. + + + + + This object is a manager which offers requests to retrieve a window address + for a toplevel. + + + + + This request has been edited to remove a compile dep. + + + + + + Get the window address for a wlr toplevel. + + + + + + + + All objects created by the manager will still remain valid, until their appropriate destroy + request has been called. + + + + + + + This object represents a mapping of a (wlr) toplevel to a window address. + + Once created, the `window_address` event will be sent containing the address of the window + associated with the toplevel. + Should the mapping fail, the `failed` event will be sent. + + + + + The full 64bit window address. The `address` field contains the lower 32 bits whilst the + `address_hi` contains the upper 32 bits + + + + + + + + The mapping of the toplevel to a window address failed. Most likely the window does not + exist (anymore). + + + + + + Destroy the handle. This request can be sent at any time by the client. + + + + diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp new file mode 100644 index 00000000..93c924c6 --- /dev/null +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp @@ -0,0 +1,53 @@ +#include "hyprland_toplevel.hpp" + +#include +#include +#include +#include + +#include "toplevel_mapping.hpp" +#include "../../toplevel_management/handle.hpp" +#include "../../toplevel_management/qml.hpp" + +using namespace qs::wayland::toplevel_management; +using namespace qs::wayland::toplevel_management::impl; + +namespace qs::hyprland::ipc { + +HyprlandToplevel::HyprlandToplevel(Toplevel* toplevel) + : QObject(toplevel) + , handle(toplevel->implHandle()) { + auto* instance = HyprlandToplevelMappingManager::instance(); + auto addr = instance->getToplevelAddress(handle); + + if (addr != 0) this->setAddress(addr); + else { + QObject::connect( + instance, + &HyprlandToplevelMappingManager::toplevelAddressed, + this, + &HyprlandToplevel::onToplevelAddressed + ); + } +} + +void HyprlandToplevel::onToplevelAddressed(ToplevelHandle* handle, quint64 address) { + if (handle == this->handle) { + this->setAddress(address); + QObject::disconnect(HyprlandToplevelMappingManager::instance(), nullptr, this, nullptr); + } +} + +void HyprlandToplevel::setAddress(quint64 address) { + this->mAddress = QString::number(address, 16); + emit this->addressChanged(); +} + +HyprlandToplevel* HyprlandToplevel::qmlAttachedProperties(QObject* object) { + if (auto* toplevel = qobject_cast(object)) { + return new HyprlandToplevel(toplevel); + } else { + return nullptr; + } +} +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp new file mode 100644 index 00000000..2cc70a5a --- /dev/null +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../../toplevel_management/qml.hpp" + +namespace qs::hyprland::ipc { + +//! Exposes Hyprland window address for a Toplevel +/// Attached object of @@Quickshell.Wayland.Toplevel which exposes +/// a Hyprland window address for the window. +class HyprlandToplevel: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + QML_ATTACHED(HyprlandToplevel); + /// Hexadecimal Hyprland window address. Will be an empty string until + /// the address is reported. + Q_PROPERTY(QString address READ address NOTIFY addressChanged); + +public: + explicit HyprlandToplevel(qs::wayland::toplevel_management::Toplevel* toplevel); + + [[nodiscard]] QString address() { return this->mAddress; } + + static HyprlandToplevel* qmlAttachedProperties(QObject* object); + +signals: + void addressChanged(); + +private slots: + void onToplevelAddressed( + qs::wayland::toplevel_management::impl::ToplevelHandle* handle, + quint64 address + ); + +private: + void setAddress(quint64 address); + + QString mAddress; + // doesn't have to be nulled on destroy, only used for comparison + qs::wayland::toplevel_management::impl::ToplevelHandle* handle; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/toplevel_mapping.cpp b/src/wayland/hyprland/ipc/toplevel_mapping.cpp new file mode 100644 index 00000000..dadd7d89 --- /dev/null +++ b/src/wayland/hyprland/ipc/toplevel_mapping.cpp @@ -0,0 +1,83 @@ +#include "toplevel_mapping.hpp" + +#include +#include +#include +#include +#include + +#include "../../toplevel_management/manager.hpp" + +using namespace qs::wayland::toplevel_management::impl; + +namespace qs::hyprland::ipc { + +HyprlandToplevelMappingHandle::~HyprlandToplevelMappingHandle() { + if (this->isInitialized()) this->destroy(); +} + +void HyprlandToplevelMappingHandle::hyprland_toplevel_window_mapping_handle_v1_window_address( + quint32 addressHi, + quint32 addressLo +) { + auto address = static_cast(addressHi) << 32 | addressLo; + HyprlandToplevelMappingManager::instance()->assignAddress(this->handle, address); + delete this; +} + +void HyprlandToplevelMappingHandle::hyprland_toplevel_window_mapping_handle_v1_failed() { + delete this; +} + +HyprlandToplevelMappingManager::HyprlandToplevelMappingManager() + : QWaylandClientExtensionTemplate(1) { + this->initialize(); + + if (!this->isInitialized()) { + qWarning() << "Compositor does not support hyprland-toplevel-mapping-v1." + "It will not be possible to derive hyprland addresses from toplevels."; + return; + } + + QObject::connect( + ToplevelManager::instance(), + &ToplevelManager::toplevelReady, + this, + &HyprlandToplevelMappingManager::onToplevelReady + ); + + for (auto* toplevel: ToplevelManager::instance()->readyToplevels()) { + this->onToplevelReady(toplevel); + } +} + +void HyprlandToplevelMappingManager::onToplevelReady(ToplevelHandle* handle) { + QObject::connect( + handle, + &QObject::destroyed, + this, + &HyprlandToplevelMappingManager::onToplevelDestroyed + ); + + new HyprlandToplevelMappingHandle(handle, this->get_window_for_toplevel_wlr(handle->object())); +} + +void HyprlandToplevelMappingManager::assignAddress(ToplevelHandle* handle, quint64 address) { + this->addresses.insert(handle, address); + emit this->toplevelAddressed(handle, address); +} + +void HyprlandToplevelMappingManager::onToplevelDestroyed(QObject* object) { + this->addresses.remove(static_cast(object)); // NOLINT +} + +quint64 HyprlandToplevelMappingManager::getToplevelAddress(ToplevelHandle* handle) const { + return this->addresses.value(handle); +} + +HyprlandToplevelMappingManager* HyprlandToplevelMappingManager::instance() { + static auto* instance = new HyprlandToplevelMappingManager(); + return instance; +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/toplevel_mapping.hpp b/src/wayland/hyprland/ipc/toplevel_mapping.hpp new file mode 100644 index 00000000..31eeade9 --- /dev/null +++ b/src/wayland/hyprland/ipc/toplevel_mapping.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "wayland-hyprland-toplevel-mapping-v1-client-protocol.h" + +namespace qs::hyprland::ipc { + +class HyprlandToplevelMappingHandle: QtWayland::hyprland_toplevel_window_mapping_handle_v1 { +public: + explicit HyprlandToplevelMappingHandle( + qs::wayland::toplevel_management::impl::ToplevelHandle* handle, + ::hyprland_toplevel_window_mapping_handle_v1* mapping + ) + : QtWayland::hyprland_toplevel_window_mapping_handle_v1(mapping) + , handle(handle) {} + + ~HyprlandToplevelMappingHandle() override; + Q_DISABLE_COPY_MOVE(HyprlandToplevelMappingHandle); + +protected: + void hyprland_toplevel_window_mapping_handle_v1_window_address( + quint32 addressHi, + quint32 addressLo + ) override; + + void hyprland_toplevel_window_mapping_handle_v1_failed() override; + +private: + qs::wayland::toplevel_management::impl::ToplevelHandle* handle; +}; + +class HyprlandToplevelMappingManager + : public QWaylandClientExtensionTemplate + , public QtWayland::hyprland_toplevel_mapping_manager_v1 { + Q_OBJECT; + +public: + explicit HyprlandToplevelMappingManager(); + + static HyprlandToplevelMappingManager* instance(); + + [[nodiscard]] quint64 + getToplevelAddress(qs::wayland::toplevel_management::impl::ToplevelHandle* handle) const; + +signals: + void toplevelAddressed( + qs::wayland::toplevel_management::impl::ToplevelHandle* handle, + quint64 address + ); + +private slots: + void onToplevelReady(qs::wayland::toplevel_management::impl::ToplevelHandle* handle); + void onToplevelDestroyed(QObject* object); + +private: + void + assignAddress(qs::wayland::toplevel_management::impl::ToplevelHandle* handle, quint64 address); + QHash addresses; + + friend class HyprlandToplevelMappingHandle; +}; +} // namespace qs::hyprland::ipc