GHOST/Wayland: primary clipboard support

Match X11's primary clipboard support
(typically used for MMB to paste the previous selection).
This commit is contained in:
Campbell Barton 2022-10-20 14:02:32 +11:00
parent 1e1b9eef1b
commit b0eff51fb7
Notes: blender-bot 2023-02-13 22:37:43 +01:00
Referenced by issue #76428, GHOST/Wayland Support
3 changed files with 341 additions and 7 deletions

View File

@ -376,6 +376,10 @@ elseif(WITH_GHOST_X11 OR WITH_GHOST_WAYLAND)
generate_protocol_bindings(
"${WAYLAND_PROTOCOLS_DIR}/unstable/tablet/tablet-unstable-v2.xml"
)
# Primary-selection.
generate_protocol_bindings(
"${WAYLAND_PROTOCOLS_DIR}/unstable/primary-selection/primary-selection-unstable-v1.xml"
)
add_definitions(-DWITH_GHOST_WAYLAND)

View File

@ -51,6 +51,7 @@
/* Generated by `wayland-scanner`. */
#include <pointer-constraints-unstable-v1-client-protocol.h>
#include <primary-selection-unstable-v1-client-protocol.h>
#include <relative-pointer-unstable-v1-client-protocol.h>
#include <tablet-unstable-v2-client-protocol.h>
#include <xdg-output-unstable-v1-client-protocol.h>
@ -426,6 +427,53 @@ static void gwl_xdg_decor_system_destroy(GWL_XDG_Decor_System *decor)
delete decor;
}
struct GWL_PrimarySelection_DataOffer {
struct zwp_primary_selection_offer_v1 *id = nullptr;
std::atomic<bool> in_use = false;
std::unordered_set<std::string> types;
};
struct GWL_PrimarySelection_DataSource {
struct zwp_primary_selection_source_v1 *wl_source = nullptr;
char *buffer_out = nullptr;
size_t buffer_out_len = 0;
};
/** Primary selection support. */
struct GWL_PrimarySelection {
GWL_PrimarySelection_DataSource *data_source = nullptr;
std::mutex data_source_mutex;
GWL_PrimarySelection_DataOffer *data_offer = nullptr;
std::mutex data_offer_mutex;
};
static void gwl_primary_selection_discard_offer(GWL_PrimarySelection *primary)
{
if (primary->data_offer == nullptr) {
return;
}
zwp_primary_selection_offer_v1_destroy(primary->data_offer->id);
delete primary->data_offer;
primary->data_offer = nullptr;
}
static void gwl_primary_selection_discard_source(GWL_PrimarySelection *primary)
{
GWL_PrimarySelection_DataSource *data_source = primary->data_source;
if (data_source == nullptr) {
return;
}
free(data_source->buffer_out);
if (data_source->wl_source) {
zwp_primary_selection_source_v1_destroy(data_source->wl_source);
}
delete primary->data_source;
primary->data_source = nullptr;
}
struct GWL_Seat {
GHOST_SystemWayland *system = nullptr;
@ -515,6 +563,9 @@ struct GWL_Seat {
struct GWL_DataSource *data_source = nullptr;
std::mutex data_source_mutex;
struct zwp_primary_selection_device_v1 *primary_selection_device = nullptr;
struct GWL_PrimarySelection primary_selection;
/** Last device that was active. */
uint32_t data_source_serial = 0;
};
@ -540,6 +591,8 @@ struct GWL_Display {
struct zwp_tablet_manager_v2 *tablet_manager = nullptr;
struct zwp_relative_pointer_manager_v1 *relative_pointer_manager = nullptr;
struct zwp_pointer_constraints_v1 *pointer_constraints = nullptr;
struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager = nullptr;
};
#undef LOG
@ -642,6 +695,22 @@ static void display_destroy(GWL_Display *display)
}
}
{
GWL_PrimarySelection *primary = &seat->primary_selection;
std::lock_guard lock{primary->data_offer_mutex};
gwl_primary_selection_discard_offer(primary);
}
{
GWL_PrimarySelection *primary = &seat->primary_selection;
std::lock_guard lock{primary->data_source_mutex};
gwl_primary_selection_discard_source(primary);
}
if (seat->primary_selection_device) {
zwp_primary_selection_device_v1_destroy(seat->primary_selection_device);
}
if (seat->data_device) {
wl_data_device_release(seat->data_device);
}
@ -696,6 +765,10 @@ static void display_destroy(GWL_Display *display)
zwp_pointer_constraints_v1_destroy(display->pointer_constraints);
}
if (display->primary_selection_device_manager) {
zwp_primary_selection_device_manager_v1_destroy(display->primary_selection_device_manager);
}
if (display->wl_compositor) {
wl_compositor_destroy(display->wl_compositor);
}
@ -1250,6 +1323,35 @@ static std::string read_pipe(GWL_DataOffer *data_offer,
return data;
}
static std::string read_pipe_primary(GWL_PrimarySelection_DataOffer *data_offer,
const std::string mime_receive,
std::mutex *mutex)
{
int pipefd[2];
if (UNLIKELY(pipe(pipefd) != 0)) {
return {};
}
zwp_primary_selection_offer_v1_receive(data_offer->id, mime_receive.c_str(), pipefd[1]);
close(pipefd[1]);
data_offer->in_use.store(false);
if (mutex) {
mutex->unlock();
}
/* WARNING: `data_offer_base` may be freed from now on. */
std::string data;
ssize_t len;
char buffer[4096];
while ((len = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
data.insert(data.end(), buffer, buffer + len);
}
close(pipefd[0]);
return data;
}
/**
* A target accepts an offered mime type.
*
@ -2880,6 +2982,158 @@ static const struct wl_keyboard_listener keyboard_listener = {
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Primary Selection Offer), #zwp_primary_selection_offer_v1_listener
* \{ */
static CLG_LogRef LOG_WL_PRIMARY_SELECTION_OFFER = {"ghost.wl.handle.primary_selection_offer"};
#define LOG (&LOG_WL_PRIMARY_SELECTION_OFFER)
static void primary_selection_offer_offer(void *data,
struct zwp_primary_selection_offer_v1 *id,
const char *type)
{
GWL_PrimarySelection_DataOffer *data_offer = static_cast<GWL_PrimarySelection_DataOffer *>(data);
if (data_offer->id != id) {
CLOG_INFO(LOG, 2, "offer: %p: offer for unknown selection %p of %s (skipped)", data, id, type);
return;
}
data_offer->types.insert(std::string(type));
}
static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = {
primary_selection_offer_offer,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Primary Selection Device), #zwp_primary_selection_device_v1_listener
* \{ */
static CLG_LogRef LOG_WL_PRIMARY_SELECTION_DEVICE = {"ghost.wl.handle.primary_selection_device"};
#define LOG (&LOG_WL_PRIMARY_SELECTION_DEVICE)
static void primary_selection_device_handle_data_offer(
void * /*data*/,
struct zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/,
struct zwp_primary_selection_offer_v1 *id)
{
CLOG_INFO(LOG, 2, "data_offer");
GWL_PrimarySelection_DataOffer *data_offer = new GWL_PrimarySelection_DataOffer;
data_offer->id = id;
zwp_primary_selection_offer_v1_add_listener(id, &primary_selection_offer_listener, data_offer);
}
static void primary_selection_device_handle_selection(
void *data,
struct zwp_primary_selection_device_v1 * /*zwp_primary_selection_device_v1*/,
struct zwp_primary_selection_offer_v1 *id)
{
GWL_PrimarySelection *primary = static_cast<GWL_PrimarySelection *>(data);
std::lock_guard lock{primary->data_offer_mutex};
/* Delete old data offer. */
if (primary->data_offer != nullptr) {
gwl_primary_selection_discard_offer(primary);
}
if (id == nullptr) {
CLOG_INFO(LOG, 2, "selection: (skipped)");
return;
}
CLOG_INFO(LOG, 2, "selection");
/* Get new data offer. */
GWL_PrimarySelection_DataOffer *data_offer = static_cast<GWL_PrimarySelection_DataOffer *>(
zwp_primary_selection_offer_v1_get_user_data(id));
primary->data_offer = data_offer;
auto read_selection_fn = [](GWL_PrimarySelection *primary) {
GHOST_SystemWayland *system = static_cast<GHOST_SystemWayland *>(GHOST_ISystem::getSystem());
primary->data_offer_mutex.lock();
GWL_PrimarySelection_DataOffer *data_offer = primary->data_offer;
std::string mime_receive;
for (const std::string type : {mime_text_utf8, mime_text_plain}) {
if (data_offer->types.count(type)) {
mime_receive = type;
break;
}
}
const std::string data = read_pipe_primary(
data_offer, mime_receive, &primary->data_offer_mutex);
{
std::lock_guard lock{system_clipboard_mutex};
system->clipboard_primary_set(data);
}
};
std::thread read_thread(read_selection_fn, primary);
read_thread.detach();
}
static const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = {
primary_selection_device_handle_data_offer,
primary_selection_device_handle_selection,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Primary Selection Source), #zwp_primary_selection_source_v1_listener
* \{ */
static CLG_LogRef LOG_WL_PRIMARY_SELECTION_SOURCE = {"ghost.wl.handle.primary_selection_source"};
#define LOG (&LOG_WL_PRIMARY_SELECTION_SOURCE)
static void primary_selection_source_send(void *data,
struct zwp_primary_selection_source_v1 * /*source*/,
const char * /*mime_type*/,
int32_t fd)
{
CLOG_INFO(LOG, 2, "send");
GWL_PrimarySelection *primary = static_cast<GWL_PrimarySelection *>(data);
std::lock_guard lock{primary->data_source_mutex};
GWL_PrimarySelection_DataSource *data_source = primary->data_source;
const char *const buffer = data_source->buffer_out;
if (write(fd, buffer, data_source->buffer_out_len) < 0) {
GHOST_PRINT("error writing to primary clipboard: " << std::strerror(errno) << std::endl);
}
close(fd);
}
static void primary_selection_source_cancelled(void *data,
struct zwp_primary_selection_source_v1 *source)
{
CLOG_INFO(LOG, 2, "cancelled");
GWL_PrimarySelection *primary = static_cast<GWL_PrimarySelection *>(data);
if (source == primary->data_source->wl_source) {
gwl_primary_selection_discard_source(primary);
}
}
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
primary_selection_source_send,
primary_selection_source_cancelled,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Seat), #wl_seat_listener
* \{ */
@ -2927,6 +3181,17 @@ static void seat_handle_capabilities(void *data,
seat->wl_keyboard = wl_seat_get_keyboard(wl_seat);
wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, data);
}
if (seat->system) {
zwp_primary_selection_device_manager_v1 *primary_selection_device_manager =
seat->system->wl_primary_selection_manager();
seat->primary_selection_device = zwp_primary_selection_device_manager_v1_get_device(
primary_selection_device_manager, seat->wl_seat);
zwp_primary_selection_device_v1_add_listener(seat->primary_selection_device,
&primary_selection_device_listener,
&seat->primary_selection);
}
}
static void seat_handle_name(void *data, struct wl_seat * /*wl_seat*/, const char *name)
@ -3297,6 +3562,12 @@ static void global_handle_add(void *data,
display->pointer_constraints = static_cast<zwp_pointer_constraints_v1 *>(
wl_registry_bind(wl_registry, name, &zwp_pointer_constraints_v1_interface, 1));
}
else if (!strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name)) {
display->primary_selection_device_manager =
static_cast<zwp_primary_selection_device_manager_v1 *>(wl_registry_bind(
wl_registry, name, &zwp_primary_selection_device_manager_v1_interface, 1));
}
else {
found = false;
@ -3564,20 +3835,52 @@ GHOST_TSuccess GHOST_SystemWayland::getButtons(GHOST_Buttons &buttons) const
return GHOST_kSuccess;
}
char *GHOST_SystemWayland::getClipboard(bool /*selection*/) const
char *GHOST_SystemWayland::getClipboard(bool selection) const
{
char *clipboard = static_cast<char *>(malloc(clipboard_.size() + 1));
memcpy(clipboard, clipboard_.data(), clipboard_.size() + 1);
const std::string &buf = selection ? clipboard_primary_ : clipboard_;
char *clipboard = static_cast<char *>(malloc(buf.size() + 1));
memcpy(clipboard, buf.data(), buf.size() + 1);
return clipboard;
}
void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) const
static void system_clipboard_put_primary_selection(GWL_Display *display, const char *buffer)
{
if (UNLIKELY(!display_->data_device_manager || display_->seats.empty())) {
if (!display->primary_selection_device_manager) {
return;
}
GWL_Seat *seat = display->seats[0];
GWL_PrimarySelection *primary = &seat->primary_selection;
GWL_Seat *seat = display_->seats[0];
std::lock_guard lock{primary->data_source_mutex};
gwl_primary_selection_discard_source(primary);
GWL_PrimarySelection_DataSource *data_source = new GWL_PrimarySelection_DataSource;
primary->data_source = data_source;
data_source->buffer_out_len = strlen(buffer);
data_source->buffer_out = static_cast<char *>(malloc(data_source->buffer_out_len));
std::memcpy(data_source->buffer_out, buffer, data_source->buffer_out_len);
data_source->wl_source = zwp_primary_selection_device_manager_v1_create_source(
display->primary_selection_device_manager);
zwp_primary_selection_source_v1_add_listener(
data_source->wl_source, &primary_selection_source_listener, primary);
for (const std::string &type : mime_send) {
zwp_primary_selection_source_v1_offer(data_source->wl_source, type.c_str());
}
if (seat->primary_selection_device) {
zwp_primary_selection_device_v1_set_selection(
seat->primary_selection_device, data_source->wl_source, seat->data_source_serial);
}
}
static void system_clipboard_put(GWL_Display *display, const char *buffer)
{
GWL_Seat *seat = display->seats[0];
std::lock_guard lock{seat->data_source_mutex};
@ -3590,7 +3893,7 @@ void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) c
std::memcpy(data_source->buffer_out, buffer, data_source->buffer_out_len);
data_source->wl_data_source = wl_data_device_manager_create_data_source(
display_->data_device_manager);
display->data_device_manager);
wl_data_source_add_listener(data_source->wl_data_source, &data_source_listener, seat);
@ -3604,6 +3907,20 @@ void GHOST_SystemWayland::putClipboard(const char *buffer, bool /*selection*/) c
}
}
void GHOST_SystemWayland::putClipboard(const char *buffer, bool selection) const
{
if (UNLIKELY(!display_->data_device_manager || display_->seats.empty())) {
return;
}
if (selection) {
system_clipboard_put_primary_selection(display_, buffer);
}
else {
system_clipboard_put(display_, buffer);
}
}
uint8_t GHOST_SystemWayland::getNumDisplays() const
{
return display_ ? uint8_t(display_->outputs.size()) : 0;
@ -4345,6 +4662,11 @@ wl_compositor *GHOST_SystemWayland::wl_compositor()
return display_->wl_compositor;
}
struct zwp_primary_selection_device_manager_v1 *GHOST_SystemWayland::wl_primary_selection_manager()
{
return display_->primary_selection_device_manager;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
libdecor *GHOST_SystemWayland::libdecor_context()
@ -4412,6 +4734,11 @@ void GHOST_SystemWayland::clipboard_set(const std::string &clipboard)
clipboard_ = clipboard;
}
void GHOST_SystemWayland::clipboard_primary_set(const std::string &clipboard)
{
clipboard_primary_ = clipboard;
}
void GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface)
{
#define SURFACE_CLEAR_PTR(surface_test) \

View File

@ -158,6 +158,7 @@ class GHOST_SystemWayland : public GHOST_System {
struct wl_display *wl_display();
struct wl_compositor *wl_compositor();
struct zwp_primary_selection_device_manager_v1 *wl_primary_selection_manager();
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
libdecor *libdecor_context();
@ -173,6 +174,7 @@ class GHOST_SystemWayland : public GHOST_System {
/* WAYLAND utility functions. */
void clipboard_set(const std::string &clipboard);
void clipboard_primary_set(const std::string &clipboard);
/** Clear all references to this surface to prevent accessing NULL pointers. */
void window_surface_unref(const wl_surface *wl_surface);
@ -192,4 +194,5 @@ class GHOST_SystemWayland : public GHOST_System {
private:
struct GWL_Display *display_;
std::string clipboard_;
std::string clipboard_primary_;
};