/*
* Copyright © 2020 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 "gtk-glue.hh"
#include "widget.hh"
#include "vteinternal.hh"
#include
#include
#include
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"};
}
#if VTE_GTK == 3
class Clipboard::Offer {
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
{
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
}
}
private:
std::shared_ptr m_clipboard;
OfferGetCallback m_get_callback;
OfferClearCallback m_clear_callback;
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 [html, len] = text_to_utf16_mozilla(*str);
// This makes yet another copy of the data... :(
if (html) {
gtk_selection_data_set(data,
gtk_selection_data_get_target(data),
// or gdk_atom_intern_static_string("text/html"),
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("text/html"),
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();
}
}
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
#endif /* VTE_GTK == 3 */
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
{
#if VTE_GTK == 3
auto platform = request->clipboard().platform();
gtk_clipboard_request_text(platform,
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 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 request = std::unique_ptr{reinterpret_cast(data)};
request->dispatch(text);
}
#endif /* VTE_GTK */
}; // class Clipboard::Request
void
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
{
#if VTE_GTK == 3
gtk_clipboard_set_text(platform(), text.data(), text.size());
#endif
}
void
Clipboard::request_text(RequestDoneCallback done_callback,
RequestFailedCallback failed_callback) /* throws */
{
Request::run(std::make_unique(*this, done_callback, failed_callback));
}
} // namespace vte::platform