--- a/plugins/protocols/xdg-activation.cpp +++ b/plugins/protocols/xdg-activation.cpp @@ -9,6 +9,10 @@ #include #include #include "config.h" +#include +#include "xdg-activation.hpp" +#include +#include class wayfire_xdg_activation_protocol_impl : public wf::plugin_interface_t { @@ -21,6 +25,27 @@ void init() override { xdg_activation = wlr_xdg_activation_v1_create(wf::get_core().display); + creator_logger = wl_display_add_protocol_logger(wf::get_core().display, + [] (void *data, wl_protocol_logger_type direction, const wl_protocol_logger_message *message) + { + if ((direction == WL_PROTOCOL_LOGGER_REQUEST) && + !std::strcmp(wl_resource_get_class(message->resource), "xdg_activation_token_v1") && + !std::strcmp(message->message->name, "commit")) + { + auto self = static_cast(data); + auto token = static_cast( + wl_resource_get_user_data(message->resource)); + if (token && (token->activation == self->xdg_activation) && !self->records.count(token)) + { + self->observe_token(token, + wl_resource_get_client(message->resource)); + } + } + }, this); + if (!creator_logger) + { + throw std::runtime_error("xdg-activation: required creator logger unavailable"); + } if (timeout >= 0) { xdg_activation->token_timeout_msec = 1000 * timeout; @@ -32,10 +57,16 @@ void fini() override { + if (creator_logger) + { + wl_protocol_logger_destroy(creator_logger); + creator_logger = nullptr; + } xdg_activation_request_activate.disconnect(); xdg_activation_new_token.disconnect(); xdg_activation_token_destroy.disconnect(); last_token = nullptr; + records.clear(); // token records disconnect both listeners through RAII } bool is_unloadable() override @@ -44,11 +75,95 @@ } private: + struct token_record + { + struct listener_hook + { + wl_listener listener; + token_record *record; + } client_destroy, token_destroy; + wayfire_xdg_activation_protocol_impl *owner; + wlr_xdg_activation_token_v1 *token; + wl_client *creator; + uint64_t id; + bool committed = false; + + token_record(wayfire_xdg_activation_protocol_impl *owner, + wlr_xdg_activation_token_v1 *token, wl_client *creator, uint64_t id) : + owner(owner), token(token), creator(creator), id(id) + { + client_destroy.record = token_destroy.record = this; + wl_list_init(&client_destroy.listener.link); + wl_list_init(&token_destroy.listener.link); + client_destroy.listener.notify = [] (wl_listener *listener, void*) + { + auto record = reinterpret_cast(listener)->record; + wl_list_remove(&record->client_destroy.listener.link); + wl_list_init(&record->client_destroy.listener.link); + record->creator = nullptr; + wf::xdg_activation_creator_destroyed_signal event{record->id}; + wf::get_core().emit(&event); + }; + token_destroy.listener.notify = [] (wl_listener *listener, void*) + { + auto record = reinterpret_cast(listener)->record; + // wlroots requires an empty listener list when destruction returns. + wl_list_remove(&record->token_destroy.listener.link); + wl_list_init(&record->token_destroy.listener.link); + auto owner = record->owner; + wf::xdg_activation_token_destroyed_signal event{record->id, + record->committed ? wlr_xdg_activation_token_v1_get_name(record->token) : nullptr, + record->committed}; + wf::get_core().emit(&event); + auto token = record->token; + owner->records.erase(token); + }; + if (creator) + { + wl_client_add_destroy_listener(creator, &client_destroy.listener); + } + wl_signal_add(&token->events.destroy, &token_destroy.listener); + } + + ~token_record() + { + wl_list_remove(&client_destroy.listener.link); + wl_list_init(&client_destroy.listener.link); + wl_list_remove(&token_destroy.listener.link); + wl_list_init(&token_destroy.listener.link); + } + }; + + std::unordered_map> records; + uint64_t next_id = 0; + + token_record *observe_token(wlr_xdg_activation_token_v1 *token, wl_client *creator = nullptr) + { + auto it = records.find(token); + if (it != records.end()) + { + return it->second.get(); + } + + auto record = std::make_unique(this, token, creator, ++next_id); + auto result = record.get(); + records.emplace(token, std::move(record)); + return result; + } + void set_callbacks() { xdg_activation_request_activate.set_callback([this] (void *data) { auto event = static_cast(data); + auto record = observe_token(event->token); // compositor tokens have no creator + // A token reaching request_activate is initialized even if it was + // created by the compositor without a client new_token event. + record->committed = true; + auto view = wf::wl_surface_to_wayfire_view(event->surface->resource); + wf::xdg_activation_request_signal observed{record->id, + wlr_xdg_activation_token_v1_get_name(event->token), record->creator, event->surface, view}; + wf::get_core().emit(&observed); // before every focus-policy return if (!event->token->seat) { @@ -64,7 +179,6 @@ last_token = nullptr; // avoid reusing the same token - wayfire_view view = wf::wl_surface_to_wayfire_view(event->surface->resource); if (!view) { LOGE("Could not get view"); @@ -85,6 +199,11 @@ xdg_activation_new_token.set_callback([this] (void *data) { auto token = static_cast(data); + auto record = observe_token(token); // never guess an absent creator + record->committed = true; + wf::xdg_activation_token_created_signal observed{record->id, + wlr_xdg_activation_token_v1_get_name(token), record->creator, token->surface, token->seat}; + wf::get_core().emit(&observed); // before check_surface can clear the seat if (!token->seat) { // note: for a valid seat, wlroots already checks that the serial is valid @@ -125,7 +244,8 @@ } }; - struct wlr_xdg_activation_v1 *xdg_activation; + struct wlr_xdg_activation_v1 *xdg_activation = nullptr; + wl_protocol_logger *creator_logger = nullptr; wf::wl_listener_wrapper xdg_activation_request_activate; wf::wl_listener_wrapper xdg_activation_new_token; wf::wl_listener_wrapper xdg_activation_token_destroy; --- /dev/null +++ b/plugins/protocols/xdg-activation.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +#pragma once +#include +#include + +struct wl_client; +struct wlr_surface; +struct wlr_seat; + +namespace wf +{ +// SrcUX launch-origin v1 contract, emitted on core. Pointers and token names are borrowed +// for synchronous observation only. Consumers must not change token/seat state, +// destroy activation objects, or retain raw client/surface pointers without +// their own destroy listeners. A null creator never authorizes a client origin. +// IDs are compositor-local correlation IDs; token names must never be logged. +struct xdg_activation_token_created_signal +{ + uint64_t id; + const char *token; + wl_client *creator; + wlr_surface *source; + wlr_seat *seat; +}; + +struct xdg_activation_token_destroyed_signal +{ + uint64_t id; + const char *token; // null for a rejected commit + bool committed; +}; + +struct xdg_activation_request_signal +{ + uint64_t id; + const char *token; + wl_client *creator; + wlr_surface *target; + wayfire_view view; // may be null, or exist before mapping +}; + +struct xdg_activation_creator_destroyed_signal +{ + uint64_t id; // clear pending origins for this token; copied values survive +}; +} --- a/plugins/protocols/meson.build +++ b/plugins/protocols/meson.build @@ -15,3 +15,5 @@ endforeach install_headers(['input-method-v1.hpp'], subdir: 'wayfire/plugins/input-method-v1/') + +install_headers(['xdg-activation.hpp'], subdir: 'wayfire/plugins/xdg-activation/')