From b939bc12ce80bf5c8ce3d8b94d5b1a261efd4e72 Mon Sep 17 00:00:00 2001 From: Christian Persch Date: Thu, 15 Sep 2022 17:50:48 +0200 Subject: widget: Implement clipboard for gtk4 Backported from master, comprising of commits: commit b72b397be45045bc5464ba8ed889f6fb2b39989a commit 7bc143b3e540aa1a6a07dbad45d6caf8c0755a7e commit fb604fe287abf64645a83ef27e2c05da14fbbae0 Fixes: https://gitlab.gnome.org/GNOME/vte/-/issues/2557 --- src/clipboard-gtk.cc | 545 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/clipboard-gtk.hh | 15 +- src/glib-glue.hh | 1 + src/gtk-glue.hh | 1 + src/vte.cc | 4 +- src/widget.cc | 5 +- src/widget.hh | 3 +- 7 files changed, 537 insertions(+), 37 deletions(-) diff --git a/src/clipboard-gtk.cc b/src/clipboard-gtk.cc index 402e3a41..e8de8729 100644 --- a/src/clipboard-gtk.cc +++ b/src/clipboard-gtk.cc @@ -1,5 +1,5 @@ /* - * Copyright © 2020 Christian Persch + * Copyright © 2020, 2022 Christian Persch * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published @@ -18,6 +18,7 @@ #include "config.h" #include "clipboard-gtk.hh" +#include "glib-glue.hh" #include "gtk-glue.hh" #include "widget.hh" #include "vteinternal.hh" @@ -26,6 +27,9 @@ #include #include +#define MIME_TYPE_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8" +#define MIME_TYPE_TEXT_HTML_UTF16 "text/html" + namespace vte::platform { // Note: @@ -64,9 +68,10 @@ Clipboard::Clipboard(Widget& delegate, throw std::runtime_error{"Failed to create clipboard"}; } -#if VTE_GTK == 3 - class Clipboard::Offer { +#if VTE_GTK == 4 + friend class ContentProvider; +#endif public: Offer(Clipboard& clipboard, OfferGetCallback get_callback, @@ -82,30 +87,15 @@ public: auto& clipboard() const noexcept { return *m_clipboard; } static void run(std::unique_ptr offer, - ClipboardFormat format) noexcept - { - auto [targets, n_targets] = targets_for_format(format); - - // Transfers ownership of *offer to the clipboard. If setting succeeds, - // the clipboard will own *offer until the clipboard_data_clear_cb - // callback is called. - // If setting the clipboard fails, the clear callback will never be - // called. - if (gtk_clipboard_set_with_data(offer->clipboard().platform(), - targets, n_targets, - clipboard_get_cb, - clipboard_clear_cb, - offer.get())) { - gtk_clipboard_set_can_store(offer->clipboard().platform(), targets, n_targets); - offer.release(); // transferred to clipboard above - } - } + ClipboardFormat format) noexcept; private: std::shared_ptr m_clipboard; OfferGetCallback m_get_callback; OfferClearCallback m_clear_callback; +#if VTE_GTK == 3 + void dispatch_get(ClipboardFormat format, GtkSelectionData* data) noexcept try @@ -219,6 +209,7 @@ private: } } +#endif /* VTE_GTK == 3 */ static std::pair text_to_utf16_mozilla(std::string_view const& str) noexcept @@ -237,7 +228,462 @@ private: }; // class Clipboard::Offer -#endif /* VTE_GTK == 3 */ + +#if VTE_GTK == 4 + +static void* task_tag; + +using VteContentProvider = GdkContentProvider; + +class ContentProvider { +public: + ContentProvider(VteContentProvider* native) + : m_native{native} + { + } + + ContentProvider() = default; + ~ContentProvider() = default; + + ContentProvider(ContentProvider const&) = delete; + ContentProvider(ContentProvider&&) = delete; + + ContentProvider& operator=(ContentProvider const&) = delete; + ContentProvider& operator=(ContentProvider&&) = delete; + + void take_offer(std::unique_ptr offer) + { + m_offer = std::move(offer); + } + + void set_format(ClipboardFormat format) + { + m_format = format; + m_content_formats = format_to_content_formats(format); + } + + void content_changed() + { + } + + void attach_clipboard(GdkClipboard* gdk_clipboard) + { + } + + void + detach_clipboard(GdkClipboard* gdk_clipboard) + { + if (auto const delegate = m_offer->clipboard().m_delegate.lock()) { + (*delegate.*m_offer->m_clear_callback)(m_offer->clipboard()); + } + } + + GdkContentFormats* + ref_formats() + { + return m_content_formats ? gdk_content_formats_ref(m_content_formats.get()) : nullptr; + } + + GdkContentFormats* + ref_storable_formats() + { + return format_to_content_formats(ClipboardFormat::TEXT).release(); + } + + void + write_mime_type_async(char const* mime_type, + GOutputStream* stream, + int io_priority, + GCancellable* cancellable, + GAsyncReadyCallback callback, + void* user_data) + { + auto task = vte::glib::take_ref(g_task_new(m_native, cancellable, callback, user_data)); + g_task_set_priority(task.get(), io_priority); + g_task_set_source_tag(task.get(), &task_tag); + g_task_set_name(task.get(), "vte-content-provider-write-async"); + + auto const format = format_from_mime_type(mime_type); + if (format == ClipboardFormat::INVALID) + return g_task_return_new_error(task.get(), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Unknown format"); + + if (auto const delegate = m_offer->clipboard().m_delegate.lock()) { + auto str = (*delegate.*m_offer->m_get_callback)(m_offer->clipboard(), format); + if (!str) + return g_task_return_new_error(task.get(), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Nothing on offer"); + + auto bytes = vte::Freeable{}; + switch (format_from_mime_type(mime_type)) { + case ClipboardFormat::TEXT: { + bytes = vte::take_freeable(g_bytes_new_with_free_func(g_strndup(str->data(), str->size()), + str->size(), + g_free, nullptr)); + break; + } + + case ClipboardFormat::HTML: { + auto [html, len] = m_offer->text_to_utf16_mozilla(*str); + + // This makes yet another copy of the data... :( + if (html) { + bytes = vte::take_freeable(g_bytes_new_with_free_func(html.release(), len, g_free, nullptr)); + break; + } else { + return g_task_return_new_error(task.get(), G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Invalid data"); + } + + break; + } + case ClipboardFormat::INVALID: + default: + break; + } + + if (bytes) { + auto provider = vte::glib::take_ref(gdk_content_provider_new_for_bytes(mime_type, bytes.release())); + return gdk_content_provider_write_mime_type_async(provider.get(), + mime_type, + stream, + io_priority, + cancellable, + write_mime_type_async_done_cb, + task.release()); // transfer + } + } + + return g_task_return_new_error(task.get(), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Offer expired"); + } + + bool + write_mime_type_finish(GAsyncResult* result, + GError** error) + { + auto const task = G_TASK(result); + return g_task_propagate_boolean(task, error); + } + + bool + get_value(GValue* value, + GError** error) + { + if (G_VALUE_HOLDS(value, G_TYPE_STRING)) { + if (auto const delegate = m_offer->clipboard().m_delegate.lock()) { + auto const str = (*delegate.*m_offer->m_get_callback)(m_offer->clipboard(), ClipboardFormat::TEXT); + if (!str) + return false; + + g_value_take_string(value, g_strndup(str->data(), str->size())); + return true; + } + } + + return false; + } + + void + offer() noexcept + { + gdk_clipboard_set_content(m_offer->clipboard().platform(), m_native); + } + + +private: + VteContentProvider* m_native{nullptr}; /* unowned */ + + std::unique_ptr m_offer; + + ClipboardFormat m_format{ClipboardFormat::INVALID}; + vte::Freeable m_content_formats; + + vte::Freeable + format_to_content_formats(ClipboardFormat format) noexcept + { + auto builder = vte::take_freeable(gdk_content_formats_builder_new()); + + switch (format) { + case ClipboardFormat::TEXT: + gdk_content_formats_builder_add_mime_type(builder.get(), + MIME_TYPE_TEXT_PLAIN_UTF8); + break; + case ClipboardFormat::HTML: + gdk_content_formats_builder_add_mime_type(builder.get(), + MIME_TYPE_TEXT_HTML_UTF16); + break; + case ClipboardFormat::INVALID: + default: + __builtin_unreachable(); + } + + return vte::take_freeable(gdk_content_formats_builder_to_formats(builder.release())); + } + + ClipboardFormat + format_from_mime_type(std::string_view const& mime_type) noexcept + { + if (mime_type == MIME_TYPE_TEXT_PLAIN_UTF8) + return ClipboardFormat::TEXT; + else if (mime_type == MIME_TYPE_TEXT_HTML_UTF16) + return ClipboardFormat::HTML; + else + return ClipboardFormat::INVALID; + } + + static void + write_mime_type_async_done_cb(GObject* source, + GAsyncResult* result, + void* user_data) noexcept + try + { + auto const provider = GDK_CONTENT_PROVIDER(source); + auto const task = vte::glib::take_ref(reinterpret_cast(user_data)); // ref added on ::write_mime_type_async + + auto error = vte::glib::Error{}; + if (!gdk_content_provider_write_mime_type_finish(provider, result, error)) { + return g_task_return_error(task.get(), error.release()); + } + + return g_task_return_boolean(task.get(), true); + } + catch (...) + { + vte::log_exception(); + } + +}; // class ContentProvider + +#define VTE_TYPE_CONTENT_PROVIDER (vte_content_provider_get_type()) +#define VTE_CONTENT_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), VTE_TYPE_CONTENT_PROVIDER, VteContentProvider)) +#define VTE_CONTENT_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), VTE_TYPE_CONTENT_PROVIDER, VteContentProviderClass)) +#define VTE_IS_CONTENT_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), VTE_TYPE_CONTENT_PROVIDER)) +#define VTE_IS_CONTENT_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), VTE_TYPE_CONTENT_PROVIDER)) +#define VTE_CONTENT_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), VTE_TYPE_CONTENT_PROVIDER, VteContentProviderClass)) + +using VteContentProviderClass = GdkContentProviderClass; + +static GType vte_content_provider_get_type(void); + +G_DEFINE_TYPE_WITH_CODE(VteContentProvider, vte_content_provider, GDK_TYPE_CONTENT_PROVIDER, + { + VteContentProvider_private_offset = + g_type_add_instance_private(g_define_type_id, sizeof(ContentProvider)); + }); + +template +static inline auto +IMPL(T* that) +{ + auto const pthat = reinterpret_cast(that); + return std::launder(reinterpret_cast(vte_content_provider_get_instance_private(pthat))); +} + +static void +vte_content_provider_content_changed(GdkContentProvider* provider) noexcept +try +{ + GDK_CONTENT_PROVIDER_CLASS(vte_content_provider_parent_class)->content_changed(provider); + + IMPL(provider)->content_changed(); +} +catch (...) +{ + vte::log_exception(); +} + +static void +vte_content_provider_attach_clipboard(GdkContentProvider* provider, + GdkClipboard* clipboard) noexcept +try +{ + GDK_CONTENT_PROVIDER_CLASS(vte_content_provider_parent_class)->attach_clipboard(provider, + clipboard); + + IMPL(provider)->attach_clipboard(clipboard); +} +catch (...) +{ + vte::log_exception(); +} + +static void +vte_content_provider_detach_clipboard(GdkContentProvider* provider, + GdkClipboard* clipboard) noexcept +try +{ + GDK_CONTENT_PROVIDER_CLASS(vte_content_provider_parent_class)->detach_clipboard(provider, + clipboard); + + IMPL(provider)->detach_clipboard(clipboard); +} +catch (...) +{ + vte::log_exception(); +} + +static GdkContentFormats* +vte_content_provider_ref_formats(GdkContentProvider* provider) noexcept +try +{ + return IMPL(provider)->ref_formats(); +} +catch (...) +{ + vte::log_exception(); + return nullptr; +} + +static GdkContentFormats* +vte_content_provider_ref_storable_formats(GdkContentProvider* provider) noexcept +try +{ + return IMPL(provider)->ref_storable_formats(); +} +catch (...) +{ + vte::log_exception(); + return nullptr; +} + +static void +vte_content_provider_write_mime_type_async(GdkContentProvider* provider, + char const* mime_type, + GOutputStream* stream, + int io_priority, + GCancellable* cancellable, + GAsyncReadyCallback callback, + void* user_data) noexcept +try +{ + return IMPL(provider)->write_mime_type_async(mime_type, + stream, + io_priority, + cancellable, + callback, user_data); +} +catch (...) +{ + vte::log_exception(); +} + +static gboolean +vte_content_provider_write_mime_type_finish(GdkContentProvider* provider, + GAsyncResult* result, + GError** error) noexcept +try +{ + assert(g_task_is_valid(result, provider)); + assert(g_task_get_source_tag(G_TASK(result)) == &task_tag); + + return IMPL(provider)->write_mime_type_finish(result, error); +} +catch (...) +{ + vte::glib::set_error_from_exception(error); + return false; +} + +static gboolean +vte_content_provider_get_value(GdkContentProvider* provider, + GValue* value, + GError** error) noexcept +try +{ + if (IMPL(provider)->get_value(value, error)) + return true; + + return GDK_CONTENT_PROVIDER_CLASS(vte_content_provider_parent_class)->get_value(provider, + value, + error); +} +catch (...) +{ + vte::glib::set_error_from_exception(error); + return false; +} + +static void +vte_content_provider_init(VteContentProvider* provider) +try +{ + auto place = vte_content_provider_get_instance_private(provider); + new (place) ContentProvider{provider}; +} +catch (...) +{ + vte::log_exception(); + g_error("Failed to create ContentProvider\n"); +} + +static void +vte_content_provider_finalize(GObject* object) noexcept +{ + IMPL(object)->~ContentProvider(); + + G_OBJECT_CLASS(vte_content_provider_parent_class)->finalize(object); +} + +static void +vte_content_provider_class_init(VteContentProviderClass *klass) +{ + auto gobject_class = G_OBJECT_CLASS(klass); + gobject_class->finalize = vte_content_provider_finalize; + + auto provider_class = GDK_CONTENT_PROVIDER_CLASS(klass); + provider_class->content_changed = vte_content_provider_content_changed; + provider_class->attach_clipboard = vte_content_provider_attach_clipboard; + provider_class->detach_clipboard = vte_content_provider_detach_clipboard; + provider_class->ref_formats = vte_content_provider_ref_formats; + provider_class->ref_storable_formats = vte_content_provider_ref_storable_formats; + provider_class->write_mime_type_async = vte_content_provider_write_mime_type_async; + provider_class->write_mime_type_finish = vte_content_provider_write_mime_type_finish; + provider_class->get_value = vte_content_provider_get_value; +} + +static auto +vte_content_provider_new(void) noexcept +{ + return reinterpret_cast + (g_object_new(VTE_TYPE_CONTENT_PROVIDER, nullptr)); +} + +#endif /* VTE_GTK == 4 */ + +void +Clipboard::Offer::run(std::unique_ptr offer, + ClipboardFormat format) noexcept +{ +#if VTE_GTK == 3 + auto [targets, n_targets] = targets_for_format(format); + + // Transfers ownership of *offer to the clipboard. If setting succeeds, + // the clipboard will own *offer until the clipboard_data_clear_cb + // callback is called. + // If setting the clipboard fails, the clear callback will never be + // called. + if (gtk_clipboard_set_with_data(offer->clipboard().platform(), + targets, n_targets, + clipboard_get_cb, + clipboard_clear_cb, + offer.get())) { + gtk_clipboard_set_can_store(offer->clipboard().platform(), targets, n_targets); + offer.release(); // transferred to clipboard above + } +#elif VTE_GTK == 4 + // It seems that to make the content available lazily (i.e. only + // generate it when the clipboard contents are requested), or + // receive a notification when said content no longer owns the + // clipboard, one has to write a new GdkContentProvider implementation. + auto const provider = vte::glib::take_ref(vte_content_provider_new()); + // Transfers ownership of *offer to the provider. + auto const impl = IMPL(provider.get()); + impl->take_offer(std::move(offer)); + impl->set_format(format); + impl->offer(); +#endif /* VTE_GTK */ +} class Clipboard::Request { public: @@ -256,11 +702,16 @@ public: static void run(std::unique_ptr request) noexcept { + auto const platform = request->clipboard().platform(); #if VTE_GTK == 3 - auto platform = request->clipboard().platform(); gtk_clipboard_request_text(platform, text_received_cb, request.release()); +#elif VTE_GTK == 4 + gdk_clipboard_read_text_async(platform, + nullptr, // cancellable + GAsyncReadyCallback(text_received_cb), + request.release()); #endif /* VTE_GTK */ } @@ -273,7 +724,7 @@ private: void dispatch(char const *text) noexcept try { - if (auto delegate = clipboard().m_delegate.lock()) { + if (auto const delegate = clipboard().m_delegate.lock()) { if (text) (*delegate.*m_done_callback)(clipboard(), {text, strlen(text)}); else @@ -289,10 +740,42 @@ private: char const* text, gpointer data) noexcept { - auto request = std::unique_ptr{reinterpret_cast(data)}; + auto const request = std::unique_ptr{reinterpret_cast(data)}; request->dispatch(text); } +#elif VTE_GTK == 4 + + void dispatch(GObject* source, + GAsyncResult* result) noexcept + try + { + // Well done gtk4 to not simply tell us also the length of the received text! + auto const text = vte::glib::take_string + (gdk_clipboard_read_text_finish(GDK_CLIPBOARD(source), + result, + nullptr)); + + if (auto const delegate = clipboard().m_delegate.lock()) { + if (text) + (*delegate.*m_done_callback)(clipboard(), {text.get(), strlen(text.get())}); + else + (*delegate.*m_failed_callback)(clipboard()); + } + } + catch (...) + { + vte::log_exception(); + } + + static void text_received_cb(GObject* source, + GAsyncResult* result, + gpointer data) noexcept + { + auto const request = std::unique_ptr{reinterpret_cast(data)}; + request->dispatch(source, result); + } + #endif /* VTE_GTK */ }; // class Clipboard::Request @@ -302,17 +785,19 @@ Clipboard::offer_data(ClipboardFormat format, OfferGetCallback get_callback, OfferClearCallback clear_callback) /* throws */ { -#if VTE_GTK == 3 Offer::run(std::make_unique(*this, get_callback, clear_callback), format); -#endif } void -Clipboard::set_text(std::string_view const& text) noexcept +Clipboard::set_text(char const* text, + size_t size) noexcept { #if VTE_GTK == 3 - gtk_clipboard_set_text(platform(), text.data(), text.size()); -#endif + gtk_clipboard_set_text(platform(), text, size); +#elif VTE_GTK == 4 + // This API sucks + gdk_clipboard_set_text(platform(), text); +#endif /* VTE_GTK */ } void diff --git a/src/clipboard-gtk.hh b/src/clipboard-gtk.hh index 45660c3e..c09fd6fe 100644 --- a/src/clipboard-gtk.hh +++ b/src/clipboard-gtk.hh @@ -31,7 +31,10 @@ namespace vte::platform { enum class ClipboardFormat { TEXT, - HTML + HTML, +#if VTE_GTK == 4 + INVALID = -1 +#endif }; enum class ClipboardType { @@ -39,7 +42,14 @@ enum class ClipboardType { PRIMARY = 1 }; +#if VTE_GTK == 4 +class ContentProvider; +#endif + class Clipboard : public std::enable_shared_from_this { +#if VTE_GTK == 4 + friend class ContentProvider; +#endif public: Clipboard(Widget& delegate, ClipboardType type) /* throws */; @@ -69,7 +79,8 @@ public: OfferGetCallback get_callback, OfferClearCallback clear_callback) /* throws */; - void set_text(std::string_view const& text) noexcept; + void set_text(char const* text, + size_t size) noexcept; void request_text(RequestDoneCallback done_callback, RequestFailedCallback failed_callback) /* throws */; diff --git a/src/glib-glue.hh b/src/glib-glue.hh index 5ddd94a8..c7839cfc 100644 --- a/src/glib-glue.hh +++ b/src/glib-glue.hh @@ -278,6 +278,7 @@ bool set_error_from_exception(GError** error namespace vte { +VTE_DECLARE_FREEABLE(GBytes, g_bytes_unref); VTE_DECLARE_FREEABLE(GOptionContext, g_option_context_free); VTE_DECLARE_FREEABLE(GVariant, g_variant_unref); diff --git a/src/gtk-glue.hh b/src/gtk-glue.hh index 71ffc9fd..443e41a6 100644 --- a/src/gtk-glue.hh +++ b/src/gtk-glue.hh @@ -31,6 +31,7 @@ VTE_DECLARE_FREEABLE(GtkTargetList, gtk_target_list_unref); #if VTE_GTK == 4 VTE_DECLARE_FREEABLE(GdkContentFormats, gdk_content_formats_unref); +VTE_DECLARE_FREEABLE(GdkContentFormatsBuilder, gdk_content_formats_builder_unref); #endif /* VTE_GTK == 4 */ } // namespace vte diff --git a/src/vte.cc b/src/vte.cc index 6cfe3b26..a8671845 100644 --- a/src/vte.cc +++ b/src/vte.cc @@ -7863,8 +7863,8 @@ Terminal::widget_unrealize() // FIXMEchpe we should check m_selection_format[sel] // and also put text/html on if it's HTML format widget()->clipboard_set_text(sel_type, - {m_selection[sel]->str, - m_selection[sel]->len}); + m_selection[sel]->str, + m_selection[sel]->len); } g_string_free(m_selection[sel], TRUE); m_selection[sel] = nullptr; diff --git a/src/widget.cc b/src/widget.cc index 5cc2b8fe..b84c0037 100644 --- a/src/widget.cc +++ b/src/widget.cc @@ -628,9 +628,10 @@ Widget::clipboard_request_text(ClipboardType type) noexcept void Widget::clipboard_set_text(ClipboardType type, - std::string_view const& str) noexcept + char const* str, + size_t size) noexcept { - clipboard_get(type).set_text(str); + clipboard_get(type).set_text(str, size); } #if VTE_GTK == 4 diff --git a/src/widget.hh b/src/widget.hh index e956a611..1e798719 100644 --- a/src/widget.hh +++ b/src/widget.hh @@ -383,7 +383,8 @@ public: ClipboardFormat format) noexcept; void clipboard_request_text(ClipboardType type) noexcept; void clipboard_set_text(ClipboardType type, - std::string_view const& str) noexcept; + char const* str, + size_t size) noexcept; void paste_text(std::string_view const& text) { m_terminal->widget_paste(text); } void paste(vte::platform::ClipboardType type) { clipboard_request_text(type); } -- cgit v1.2.1