diff --git a/CMakeLists.txt b/CMakeLists.txt index b10c5d41b..4d053742b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -382,6 +382,7 @@ protocolnew("staging/color-management" "color-management-v1" false) protocolnew("staging/xdg-toplevel-tag" "xdg-toplevel-tag-v1" false) protocolnew("staging/xdg-system-bell" "xdg-system-bell-v1" false) protocolnew("staging/ext-workspace" "ext-workspace-v1" false) +protocolnew("staging/ext-data-control" "ext-data-control-v1" false) protocolwayland() diff --git a/protocols/meson.build b/protocols/meson.build index a7f728888..b52c43498 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -76,6 +76,7 @@ protocols = [ wayland_protocol_dir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml', wayland_protocol_dir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', wayland_protocol_dir / 'staging/ext-workspace/ext-workspace-v1.xml', + wayland_protocol_dir / 'staging/ext-data-control/ext-data-control-v1.xml', ] wl_protocols = [] diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp index 91691926e..b70d3b7d7 100644 --- a/src/managers/ProtocolManager.cpp +++ b/src/managers/ProtocolManager.cpp @@ -63,6 +63,7 @@ #include "../protocols/XDGTag.hpp" #include "../protocols/XDGBell.hpp" #include "../protocols/ExtWorkspace.hpp" +#include "../protocols/ExtDataDevice.hpp" #include "../helpers/Monitor.hpp" #include "../render/Renderer.hpp" @@ -190,6 +191,7 @@ CProtocolManager::CProtocolManager() { PROTO::xdgTag = makeUnique(&xdg_toplevel_tag_manager_v1_interface, 1, "XDGTag"); PROTO::xdgBell = makeUnique(&xdg_system_bell_v1_interface, 1, "XDGBell"); PROTO::extWorkspace = makeUnique(&ext_workspace_manager_v1_interface, 1, "ExtWorkspace"); + PROTO::extDataDevice = makeUnique(&ext_data_control_manager_v1_interface, 1, "ExtDataDevice"); if (*PENABLECM) PROTO::colorManagement = makeUnique(&wp_color_manager_v1_interface, 1, "ColorManagement", *PDEBUGCM); @@ -292,6 +294,7 @@ CProtocolManager::~CProtocolManager() { PROTO::xdgTag.reset(); PROTO::xdgBell.reset(); PROTO::extWorkspace.reset(); + PROTO::extDataDevice.reset(); for (auto& [_, lease] : PROTO::lease) { lease.reset(); diff --git a/src/managers/SeatManager.cpp b/src/managers/SeatManager.cpp index 67752e2aa..bc4f2dfc6 100644 --- a/src/managers/SeatManager.cpp +++ b/src/managers/SeatManager.cpp @@ -2,6 +2,7 @@ #include "../protocols/core/Seat.hpp" #include "../protocols/core/DataDevice.hpp" #include "../protocols/DataDeviceWlr.hpp" +#include "../protocols/ExtDataDevice.hpp" #include "../protocols/PrimarySelection.hpp" #include "../protocols/core/Compositor.hpp" #include "../Compositor.hpp" @@ -578,6 +579,7 @@ void CSeatManager::setCurrentSelection(SP source) { m_selection.destroySelection = source->m_events.destroy.listen([this] { setCurrentSelection(nullptr); }); PROTO::data->setSelection(source); PROTO::dataWlr->setSelection(source, false); + PROTO::extDataDevice->setSelection(source, false); } m_events.setSelection.emit(); @@ -603,6 +605,7 @@ void CSeatManager::setCurrentPrimarySelection(SP source) { m_selection.destroyPrimarySelection = source->m_events.destroy.listen([this] { setCurrentPrimarySelection(nullptr); }); PROTO::primarySelection->setSelection(source); PROTO::dataWlr->setSelection(source, true); + PROTO::extDataDevice->setSelection(source, true); } m_events.setPrimarySelection.emit(); diff --git a/src/protocols/ExtDataDevice.cpp b/src/protocols/ExtDataDevice.cpp new file mode 100644 index 000000000..c2d4c4976 --- /dev/null +++ b/src/protocols/ExtDataDevice.cpp @@ -0,0 +1,322 @@ +#include "ExtDataDevice.hpp" +#include +#include "../managers/SeatManager.hpp" +#include "core/Seat.hpp" +using namespace Hyprutils::OS; + +CExtDataOffer::CExtDataOffer(SP resource_, SP source_) : m_source(source_), m_resource(resource_) { + if UNLIKELY (!good()) + return; + + m_resource->setDestroy([this](CExtDataControlOfferV1* r) { PROTO::extDataDevice->destroyResource(this); }); + m_resource->setOnDestroy([this](CExtDataControlOfferV1* r) { PROTO::extDataDevice->destroyResource(this); }); + + m_resource->setReceive([this](CExtDataControlOfferV1* r, const char* mime, int32_t fd) { + CFileDescriptor sendFd{fd}; + if (!m_source) { + LOGM(WARN, "Possible bug: Receive on an offer w/o a source"); + return; + } + + if (m_dead) { + LOGM(WARN, "Possible bug: Receive on an offer that's dead"); + return; + } + + LOGM(LOG, "Offer {:x} asks to send data from source {:x}", (uintptr_t)this, (uintptr_t)m_source.get()); + + m_source->send(mime, std::move(sendFd)); + }); +} + +bool CExtDataOffer::good() { + return m_resource->resource(); +} + +void CExtDataOffer::sendData() { + if UNLIKELY (!m_source) + return; + + for (auto const& m : m_source->mimes()) { + m_resource->sendOffer(m.c_str()); + } +} + +CExtDataSource::CExtDataSource(SP resource_, SP device_) : m_device(device_), m_resource(resource_) { + if UNLIKELY (!good()) + return; + + m_resource->setData(this); + + m_resource->setDestroy([this](CExtDataControlSourceV1* r) { + m_events.destroy.emit(); + PROTO::extDataDevice->destroyResource(this); + }); + m_resource->setOnDestroy([this](CExtDataControlSourceV1* r) { + m_events.destroy.emit(); + PROTO::extDataDevice->destroyResource(this); + }); + + m_resource->setOffer([this](CExtDataControlSourceV1* r, const char* mime) { m_mimeTypes.emplace_back(mime); }); +} + +CExtDataSource::~CExtDataSource() { + m_events.destroy.emit(); +} + +SP CExtDataSource::fromResource(wl_resource* res) { + auto data = (CExtDataSource*)(((CExtDataControlSourceV1*)wl_resource_get_user_data(res))->data()); + return data ? data->m_self.lock() : nullptr; +} + +bool CExtDataSource::good() { + return m_resource->resource(); +} + +std::vector CExtDataSource::mimes() { + return m_mimeTypes; +} + +void CExtDataSource::send(const std::string& mime, CFileDescriptor fd) { + if (std::ranges::find(m_mimeTypes, mime) == m_mimeTypes.end()) { + LOGM(ERR, "Compositor/App bug: CExtDataSource::sendAskSend with non-existent mime"); + return; + } + + m_resource->sendSend(mime.c_str(), fd.get()); +} + +void CExtDataSource::accepted(const std::string& mime) { + if (std::ranges::find(m_mimeTypes, mime) == m_mimeTypes.end()) + LOGM(ERR, "Compositor/App bug: CExtDataSource::sendAccepted with non-existent mime"); + + // ext has no accepted +} + +void CExtDataSource::cancelled() { + m_resource->sendCancelled(); +} + +void CExtDataSource::error(uint32_t code, const std::string& msg) { + m_resource->error(code, msg); +} + +CExtDataDevice::CExtDataDevice(SP resource_) : m_resource(resource_) { + if UNLIKELY (!good()) + return; + + m_client = m_resource->client(); + + m_resource->setDestroy([this](CExtDataControlDeviceV1* r) { PROTO::extDataDevice->destroyResource(this); }); + m_resource->setOnDestroy([this](CExtDataControlDeviceV1* r) { PROTO::extDataDevice->destroyResource(this); }); + + m_resource->setSetSelection([](CExtDataControlDeviceV1* r, wl_resource* sourceR) { + auto source = sourceR ? CExtDataSource::fromResource(sourceR) : CSharedPointer{}; + if (!source) { + LOGM(LOG, "ext reset selection received"); + g_pSeatManager->setCurrentSelection(nullptr); + return; + } + + if (source && source->used()) + LOGM(WARN, "setSelection on a used resource. By protocol, this is a violation, but firefox et al insist on doing this."); + + source->markUsed(); + + LOGM(LOG, "ext manager requests selection to {:x}", (uintptr_t)source.get()); + g_pSeatManager->setCurrentSelection(source); + }); + + m_resource->setSetPrimarySelection([](CExtDataControlDeviceV1* r, wl_resource* sourceR) { + auto source = sourceR ? CExtDataSource::fromResource(sourceR) : CSharedPointer{}; + if (!source) { + LOGM(LOG, "ext reset primary selection received"); + g_pSeatManager->setCurrentPrimarySelection(nullptr); + return; + } + + if (source && source->used()) + LOGM(WARN, "setSelection on a used resource. By protocol, this is a violation, but firefox et al insist on doing this."); + + source->markUsed(); + + LOGM(LOG, "ext manager requests primary selection to {:x}", (uintptr_t)source.get()); + g_pSeatManager->setCurrentPrimarySelection(source); + }); +} + +bool CExtDataDevice::good() { + return m_resource->resource(); +} + +wl_client* CExtDataDevice::client() { + return m_client; +} + +void CExtDataDevice::sendInitialSelections() { + PROTO::extDataDevice->sendSelectionToDevice(self.lock(), g_pSeatManager->m_selection.currentSelection.lock(), false); + PROTO::extDataDevice->sendSelectionToDevice(self.lock(), g_pSeatManager->m_selection.currentPrimarySelection.lock(), true); +} + +void CExtDataDevice::sendDataOffer(SP offer) { + m_resource->sendDataOffer(offer->m_resource.get()); +} + +void CExtDataDevice::sendSelection(SP selection) { + m_resource->sendSelection(selection->m_resource.get()); +} + +void CExtDataDevice::sendPrimarySelection(SP selection) { + m_resource->sendPrimarySelection(selection->m_resource.get()); +} + +CExtDataControlManagerResource::CExtDataControlManagerResource(SP resource_) : m_resource(resource_) { + if UNLIKELY (!good()) + return; + + m_resource->setDestroy([this](CExtDataControlManagerV1* r) { PROTO::extDataDevice->destroyResource(this); }); + m_resource->setOnDestroy([this](CExtDataControlManagerV1* r) { PROTO::extDataDevice->destroyResource(this); }); + + m_resource->setGetDataDevice([this](CExtDataControlManagerV1* r, uint32_t id, wl_resource* seat) { + const auto RESOURCE = PROTO::extDataDevice->m_devices.emplace_back(makeShared(makeShared(r->client(), r->version(), id))); + + if UNLIKELY (!RESOURCE->good()) { + r->noMemory(); + PROTO::extDataDevice->m_devices.pop_back(); + return; + } + + RESOURCE->self = RESOURCE; + m_device = RESOURCE; + + for (auto const& s : m_sources) { + if (!s) + continue; + s->m_device = RESOURCE; + } + + RESOURCE->sendInitialSelections(); + + LOGM(LOG, "New ext data device bound at {:x}", (uintptr_t)RESOURCE.get()); + }); + + m_resource->setCreateDataSource([this](CExtDataControlManagerV1* r, uint32_t id) { + std::erase_if(m_sources, [](const auto& e) { return e.expired(); }); + + const auto RESOURCE = + PROTO::extDataDevice->m_sources.emplace_back(makeShared(makeShared(r->client(), r->version(), id), m_device.lock())); + + if UNLIKELY (!RESOURCE->good()) { + r->noMemory(); + PROTO::extDataDevice->m_sources.pop_back(); + return; + } + + if (!m_device) + LOGM(WARN, "New data source before a device was created"); + + RESOURCE->m_self = RESOURCE; + + m_sources.emplace_back(RESOURCE); + + LOGM(LOG, "New ext data source bound at {:x}", (uintptr_t)RESOURCE.get()); + }); +} + +bool CExtDataControlManagerResource::good() { + return m_resource->resource(); +} + +CExtDataDeviceProtocol::CExtDataDeviceProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) { + ; +} + +void CExtDataDeviceProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) { + const auto RESOURCE = m_managers.emplace_back(makeShared(makeShared(client, ver, id))); + + if UNLIKELY (!RESOURCE->good()) { + wl_client_post_no_memory(client); + m_managers.pop_back(); + return; + } + + LOGM(LOG, "New ext_data_control_manager at {:x}", (uintptr_t)RESOURCE.get()); +} + +void CExtDataDeviceProtocol::destroyResource(CExtDataControlManagerResource* resource) { + std::erase_if(m_managers, [&](const auto& other) { return other.get() == resource; }); +} + +void CExtDataDeviceProtocol::destroyResource(CExtDataSource* resource) { + std::erase_if(m_sources, [&](const auto& other) { return other.get() == resource; }); +} + +void CExtDataDeviceProtocol::destroyResource(CExtDataDevice* resource) { + std::erase_if(m_devices, [&](const auto& other) { return other.get() == resource; }); +} + +void CExtDataDeviceProtocol::destroyResource(CExtDataOffer* resource) { + std::erase_if(m_offers, [&](const auto& other) { return other.get() == resource; }); +} + +void CExtDataDeviceProtocol::sendSelectionToDevice(SP dev, SP sel, bool primary) { + if (!sel) { + if (primary) + dev->m_resource->sendPrimarySelectionRaw(nullptr); + else + dev->m_resource->sendSelectionRaw(nullptr); + return; + } + + const auto OFFER = m_offers.emplace_back(makeShared(makeShared(dev->m_resource->client(), dev->m_resource->version(), 0), sel)); + + if (!OFFER->good()) { + dev->m_resource->noMemory(); + m_offers.pop_back(); + return; + } + + OFFER->m_primary = primary; + + LOGM(LOG, "New {}offer {:x} for data source {:x}", primary ? "primary " : " ", (uintptr_t)OFFER.get(), (uintptr_t)sel.get()); + + dev->sendDataOffer(OFFER); + OFFER->sendData(); + if (primary) + dev->sendPrimarySelection(OFFER); + else + dev->sendSelection(OFFER); +} + +void CExtDataDeviceProtocol::setSelection(SP source, bool primary) { + for (auto const& o : m_offers) { + if (o->m_source && o->m_source->hasDnd()) + continue; + if (o->m_primary != primary) + continue; + o->m_dead = true; + } + + if (!source) { + LOGM(LOG, "resetting {}selection", primary ? "primary " : " "); + + for (auto const& d : m_devices) { + sendSelectionToDevice(d, nullptr, primary); + } + + return; + } + + LOGM(LOG, "New {}selection for data source {:x}", primary ? "primary" : "", (uintptr_t)source.get()); + + for (auto const& d : m_devices) { + sendSelectionToDevice(d, source, primary); + } +} + +SP CExtDataDeviceProtocol::dataDeviceForClient(wl_client* c) { + auto it = std::ranges::find_if(m_devices, [c](const auto& e) { return e->client() == c; }); + if (it == m_devices.end()) + return nullptr; + return *it; +} diff --git a/src/protocols/ExtDataDevice.hpp b/src/protocols/ExtDataDevice.hpp new file mode 100644 index 000000000..462090f33 --- /dev/null +++ b/src/protocols/ExtDataDevice.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include "WaylandProtocol.hpp" +#include "ext-data-control-v1.hpp" +#include "types/DataDevice.hpp" +#include + +class CExtDataControlManagerResource; +class CExtDataSource; +class CExtDataDevice; +class CExtDataOffer; + +class CExtDataOffer { + public: + CExtDataOffer(SP resource_, SP source); + + bool good(); + void sendData(); + + bool m_dead = false; + bool m_primary = false; + + WP m_source; + + private: + SP m_resource; + + friend class CExtDataDevice; +}; + +class CExtDataSource : public IDataSource { + public: + CExtDataSource(SP resource_, SP device_); + ~CExtDataSource(); + static SP fromResource(wl_resource*); + + bool good(); + + virtual std::vector mimes(); + virtual void send(const std::string& mime, Hyprutils::OS::CFileDescriptor fd); + virtual void accepted(const std::string& mime); + virtual void cancelled(); + virtual void error(uint32_t code, const std::string& msg); + + std::vector m_mimeTypes; + WP m_self; + WP m_device; + + private: + SP m_resource; +}; + +class CExtDataDevice { + public: + CExtDataDevice(SP resource_); + + bool good(); + wl_client* client(); + void sendInitialSelections(); + + void sendDataOffer(SP offer); + void sendSelection(SP selection); + void sendPrimarySelection(SP selection); + + WP self; + + private: + SP m_resource; + wl_client* m_client = nullptr; + + friend class CExtDataDeviceProtocol; +}; + +class CExtDataControlManagerResource { + public: + CExtDataControlManagerResource(SP resource_); + + bool good(); + + WP m_device; + std::vector> m_sources; + + private: + SP m_resource; +}; + +class CExtDataDeviceProtocol : public IWaylandProtocol { + public: + CExtDataDeviceProtocol(const wl_interface* iface, const int& ver, const std::string& name); + + virtual void bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id); + + private: + void destroyResource(CExtDataControlManagerResource* resource); + void destroyResource(CExtDataSource* resource); + void destroyResource(CExtDataDevice* resource); + void destroyResource(CExtDataOffer* resource); + + // + std::vector> m_managers; + std::vector> m_sources; + std::vector> m_devices; + std::vector> m_offers; + + // + void setSelection(SP source, bool primary); + void sendSelectionToDevice(SP dev, SP sel, bool primary); + + // + SP dataDeviceForClient(wl_client*); + + friend class CSeatManager; + friend class CExtDataControlManagerResource; + friend class CExtDataSource; + friend class CExtDataDevice; + friend class CExtDataOffer; +}; + +namespace PROTO { + inline UP extDataDevice; +};