/*
* 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
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*/
#include "config.h"
#include "clipboard-gtk.hh"
#include "glib-glue.hh"
#include "gtk-glue.hh"
#include "widget.hh"
#include "vteinternal.hh"
#include
#include
#include
#define MIME_TYPE_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
#define MIME_TYPE_TEXT_HTML_UTF8 "text/html;charset=utf-8"
#define MIME_TYPE_TEXT_HTML_UTF16 "text/html"
namespace vte::platform {
// Note:
// Each Clipboard is owned via std::shared_ptr by Widget, which drops that ref on unrealize.
// The Clipboard keeps a std::weak_ref back on Widget, and converts that to a std::shared_ptr
// via .lock() only when it wants to dispatch a callback.
// Clipboard::Offer and Clipboard::Request own their Clipboard as a std::shared_ptr.
Clipboard::Clipboard(Widget& delegate,
ClipboardType type) /* throws */
: m_delegate{delegate.weak_from_this()},
m_type{type}
{
auto display = gtk_widget_get_display(delegate.gtk());
switch (type) {
case ClipboardType::PRIMARY:
m_clipboard = vte::glib::make_ref
#if VTE_GTK == 3
(gtk_clipboard_get_for_display(display, GDK_SELECTION_PRIMARY));
#elif VTE_GTK == 4
(gdk_display_get_primary_clipboard(display));
#endif
break;
case ClipboardType::CLIPBOARD:
m_clipboard = vte::glib::make_ref
#if VTE_GTK == 3
(gtk_clipboard_get_for_display(display, GDK_SELECTION_CLIPBOARD));
#elif VTE_GTK == 4
(gdk_display_get_clipboard(display));
#endif
break;
}
if (!m_clipboard)
throw std::runtime_error{"Failed to create clipboard"};
}
class Clipboard::Offer {
#if VTE_GTK == 4
friend class ContentProvider;
#endif
public:
Offer(Clipboard& clipboard,
OfferGetCallback get_callback,
OfferClearCallback clear_callback)
: m_clipboard{clipboard.shared_from_this()},
m_get_callback{get_callback},
m_clear_callback{clear_callback}
{
}
~Offer() = default;
auto& clipboard() const noexcept { return *m_clipboard; }
static void run(std::unique_ptr offer,
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
{
if (auto delegate = clipboard().m_delegate.lock()) {
auto str = (*delegate.*m_get_callback)(clipboard(), format);
if (!str)
return;
switch (format) {
case ClipboardFormat::TEXT:
// This makes yet another copy of the data... :(
gtk_selection_data_set_text(data, str->data(), str->size());
break;
case ClipboardFormat::HTML: {
auto const target = gtk_selection_data_get_target(data);
if (target == gdk_atom_intern_static_string(MIME_TYPE_TEXT_HTML_UTF8)) {
// This makes yet another copy of the data... :(
gtk_selection_data_set(data,
target,
8,
reinterpret_cast(str->data()),
str->size());
} else if (target == gdk_atom_intern_static_string(MIME_TYPE_TEXT_HTML_UTF16)) {
auto [html, len] = text_to_utf16_mozilla(*str);
// This makes yet another copy of the data... :(
if (html) {
gtk_selection_data_set(data,
target,
16,
reinterpret_cast(html.get()),
len);
}
}
break;
}
}
}
}
catch (...)
{
vte::log_exception();
}
void dispatch_clear() noexcept
try
{
if (auto delegate = clipboard().m_delegate.lock()) {
(*delegate.*m_clear_callback)(clipboard());
}
}
catch (...)
{
vte::log_exception();
}
static void
clipboard_get_cb(GtkClipboard* clipboard,
GtkSelectionData* data,
guint info,
void* user_data) noexcept
{
if (int(info) != vte::to_integral(ClipboardFormat::TEXT) &&
int(info) != vte::to_integral(ClipboardFormat::HTML))
return;
reinterpret_cast(user_data)->dispatch_get(ClipboardFormat(info), data);
}
static void
clipboard_clear_cb(GtkClipboard* clipboard,
void* user_data) noexcept
{
// Assume ownership of the Offer, and delete it after dispatching the callback
auto offer = std::unique_ptr{reinterpret_cast(user_data)};
offer->dispatch_clear();
}
static std::pair
targets_for_format(ClipboardFormat format)
{
switch (format) {
case vte::platform::ClipboardFormat::TEXT: {
static GtkTargetEntry *text_targets = nullptr;
static int n_text_targets;
if (text_targets == nullptr) {
auto list = vte::take_freeable(gtk_target_list_new(nullptr, 0));
gtk_target_list_add_text_targets(list.get(),
vte::to_integral(ClipboardFormat::TEXT));
text_targets = gtk_target_table_new_from_list(list.get(), &n_text_targets);
}
return {text_targets, n_text_targets};
}
case vte::platform::ClipboardFormat::HTML: {
static GtkTargetEntry *html_targets = nullptr;
static int n_html_targets;
if (html_targets == nullptr) {
auto list = vte::take_freeable(gtk_target_list_new(nullptr, 0));
gtk_target_list_add_text_targets(list.get(),
vte::to_integral(ClipboardFormat::TEXT));
gtk_target_list_add(list.get(),
gdk_atom_intern_static_string(MIME_TYPE_TEXT_HTML_UTF8),
0,
vte::to_integral(ClipboardFormat::HTML));
gtk_target_list_add(list.get(),
gdk_atom_intern_static_string(MIME_TYPE_TEXT_HTML_UTF16),
0,
vte::to_integral(ClipboardFormat::HTML));
html_targets = gtk_target_table_new_from_list(list.get(), &n_html_targets);
}
return {html_targets, n_html_targets};
}
default:
g_assert_not_reached();
}
}
#endif /* VTE_GTK == 3 */
static std::pair
text_to_utf16_mozilla(std::string_view const& str) noexcept
{
// Use g_convert() instead of g_utf8_to_utf16() since the former
// adds a BOM which Mozilla requires for text/html format.
auto len = size_t{};
auto data = g_convert(str.data(), str.size(),
"UTF-16", // conver to UTF-16
"UTF-8", // convert from UTF-8
nullptr, // out bytes_read
&len,
nullptr);
return {vte::glib::take_string(data), len};
}
}; // class Clipboard::Offer
#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);
#if GLIB_CHECK_VERSION(2, 60, 0)
g_task_set_name(task.get(), "vte-content-provider-write-async");
#endif
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 const type = std::string_view{mime_type};
if (type == MIME_TYPE_TEXT_HTML_UTF8) {
bytes = vte::take_freeable(g_bytes_new_with_free_func(g_strndup(str->data(), str->size()),
str->size(),
g_free, nullptr));
} else if (type == MIME_TYPE_TEXT_HTML_UTF16) {
auto [html, len] = m_offer->text_to_utf16_mozilla(*str);
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_UTF8);
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_UTF8 ||
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:
Request(Clipboard& clipboard,
RequestDoneCallback done_callback,
RequestFailedCallback failed_callback)
: m_clipboard{clipboard.shared_from_this()},
m_done_callback{done_callback},
m_failed_callback{failed_callback}
{
}
~Request() = default;
auto& clipboard() const noexcept { return *m_clipboard; }
static void run(std::unique_ptr request) noexcept
{
auto const platform = request->clipboard().platform();
#if VTE_GTK == 3
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 */
}
private:
std::shared_ptr m_clipboard;
RequestDoneCallback m_done_callback;
RequestFailedCallback m_failed_callback;
#if VTE_GTK == 3
void dispatch(char const *text) noexcept
try
{
if (auto const delegate = clipboard().m_delegate.lock()) {
if (text)
(*delegate.*m_done_callback)(clipboard(), {text, strlen(text)});
else
(*delegate.*m_failed_callback)(clipboard());
}
}
catch (...)
{
vte::log_exception();
}
static void text_received_cb(GtkClipboard *clipboard,
char const* text,
gpointer data) noexcept
{
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
void
Clipboard::offer_data(ClipboardFormat format,
OfferGetCallback get_callback,
OfferClearCallback clear_callback) /* throws */
{
Offer::run(std::make_unique(*this, get_callback, clear_callback), format);
}
void
Clipboard::set_text(char const* text,
size_t size) noexcept
{
#if VTE_GTK == 3
gtk_clipboard_set_text(platform(), text, size);
#elif VTE_GTK == 4
// This API sucks
gdk_clipboard_set_text(platform(), text);
#endif /* VTE_GTK */
}
void
Clipboard::request_text(RequestDoneCallback done_callback,
RequestFailedCallback failed_callback) /* throws */
{
Request::run(std::make_unique(*this, done_callback, failed_callback));
}
} // namespace vte::platform