1124 lines
32 KiB
C++
1124 lines
32 KiB
C++
|
// Clip Library
|
||
|
// Copyright (c) 2018-2022 David Capello
|
||
|
//
|
||
|
// This file is released under the terms of the MIT license.
|
||
|
// Read LICENSE.txt for more information.
|
||
|
|
||
|
#include "clip.h"
|
||
|
#include "clip_lock_impl.h"
|
||
|
|
||
|
#include <xcb/xcb.h>
|
||
|
|
||
|
#include <atomic>
|
||
|
#include <algorithm>
|
||
|
#include <cassert>
|
||
|
#include <condition_variable>
|
||
|
#include <cstdint>
|
||
|
#include <cstdlib>
|
||
|
#include <cstring>
|
||
|
#include <functional>
|
||
|
#include <map>
|
||
|
#include <memory>
|
||
|
#include <mutex>
|
||
|
#include <thread>
|
||
|
#include <vector>
|
||
|
|
||
|
#if CLIP_ENABLE_IMAGE && HAVE_PNG_H
|
||
|
#include "clip_x11_png.h"
|
||
|
#endif
|
||
|
|
||
|
#define CLIP_SUPPORT_SAVE_TARGETS 1
|
||
|
|
||
|
namespace clip {
|
||
|
|
||
|
namespace {
|
||
|
|
||
|
enum CommonAtom {
|
||
|
ATOM,
|
||
|
INCR,
|
||
|
TARGETS,
|
||
|
CLIPBOARD,
|
||
|
#ifdef HAVE_PNG_H
|
||
|
MIME_IMAGE_PNG,
|
||
|
#endif
|
||
|
#ifdef CLIP_SUPPORT_SAVE_TARGETS
|
||
|
ATOM_PAIR,
|
||
|
SAVE_TARGETS,
|
||
|
MULTIPLE,
|
||
|
CLIPBOARD_MANAGER,
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
const char* kCommonAtomNames[] = {
|
||
|
"ATOM",
|
||
|
"INCR",
|
||
|
"TARGETS",
|
||
|
"CLIPBOARD",
|
||
|
#ifdef HAVE_PNG_H
|
||
|
"image/png",
|
||
|
#endif
|
||
|
#ifdef CLIP_SUPPORT_SAVE_TARGETS
|
||
|
"ATOM_PAIR",
|
||
|
"SAVE_TARGETS",
|
||
|
"MULTIPLE",
|
||
|
"CLIPBOARD_MANAGER",
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
const int kBaseForCustomFormats = 100;
|
||
|
|
||
|
class Manager {
|
||
|
public:
|
||
|
typedef std::shared_ptr<std::vector<uint8_t>> buffer_ptr;
|
||
|
typedef std::vector<xcb_atom_t> atoms;
|
||
|
typedef std::function<bool()> notify_callback;
|
||
|
|
||
|
Manager()
|
||
|
: m_lock(m_mutex, std::defer_lock)
|
||
|
, m_connection(xcb_connect(nullptr, nullptr))
|
||
|
, m_window(0)
|
||
|
, m_incr_process(false) {
|
||
|
if (!m_connection)
|
||
|
return;
|
||
|
|
||
|
const xcb_setup_t* setup = xcb_get_setup(m_connection);
|
||
|
if (!setup)
|
||
|
return;
|
||
|
|
||
|
xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data;
|
||
|
if (!screen)
|
||
|
return;
|
||
|
|
||
|
uint32_t event_mask =
|
||
|
// Just in case that some program reports SelectionNotify events
|
||
|
// with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
|
||
|
XCB_EVENT_MASK_PROPERTY_CHANGE |
|
||
|
// To receive DestroyNotify event and stop the message loop.
|
||
|
XCB_EVENT_MASK_STRUCTURE_NOTIFY;
|
||
|
|
||
|
m_window = xcb_generate_id(m_connection);
|
||
|
xcb_create_window(m_connection, 0,
|
||
|
m_window,
|
||
|
screen->root,
|
||
|
0, 0, 1, 1, 0,
|
||
|
XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
||
|
screen->root_visual,
|
||
|
XCB_CW_EVENT_MASK,
|
||
|
&event_mask);
|
||
|
|
||
|
m_thread = std::thread(
|
||
|
[this]{
|
||
|
process_x11_events();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
~Manager() {
|
||
|
#ifdef CLIP_SUPPORT_SAVE_TARGETS
|
||
|
if (!m_data.empty() &&
|
||
|
m_window &&
|
||
|
m_window == get_x11_selection_owner()) {
|
||
|
// If the CLIPBOARD_MANAGER atom is not 0, we assume that there
|
||
|
// is a clipboard manager available were we can leave our data.
|
||
|
xcb_atom_t x11_clipboard_manager = get_atom(CLIPBOARD_MANAGER);
|
||
|
if (x11_clipboard_manager) {
|
||
|
// We have to lock the m_lock mutex that will be used to wait
|
||
|
// the m_cv condition in get_data_from_selection_owner().
|
||
|
if (try_lock()) {
|
||
|
// Start the SAVE_TARGETS mechanism so the X11
|
||
|
// CLIPBOARD_MANAGER will save our clipboard data
|
||
|
// from now on.
|
||
|
get_data_from_selection_owner(
|
||
|
{ get_atom(SAVE_TARGETS) },
|
||
|
[]() -> bool { return true; },
|
||
|
x11_clipboard_manager);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
if (m_window) {
|
||
|
xcb_destroy_window(m_connection, m_window);
|
||
|
xcb_flush(m_connection);
|
||
|
}
|
||
|
|
||
|
if (m_thread.joinable())
|
||
|
m_thread.join();
|
||
|
|
||
|
if (m_connection)
|
||
|
xcb_disconnect(m_connection);
|
||
|
}
|
||
|
|
||
|
bool try_lock() {
|
||
|
bool res = m_lock.try_lock();
|
||
|
if (!res) {
|
||
|
// TODO make this configurable (the same for Windows retries)
|
||
|
for (int i=0; i<5 && !res; ++i) {
|
||
|
res = m_lock.try_lock();
|
||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||
|
}
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
void unlock() {
|
||
|
m_lock.unlock();
|
||
|
}
|
||
|
|
||
|
// Clear our data
|
||
|
void clear_data() {
|
||
|
m_data.clear();
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
m_image.reset();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
void clear() {
|
||
|
clear_data();
|
||
|
|
||
|
// As we want to clear the clipboard content, we set us as the new
|
||
|
// clipboard owner (with an empty clipboard). If this fails, we'll
|
||
|
// try to send a XCB_SELECTION_CLEAR request to the real owner
|
||
|
// (but that can fail anyway because it's a request that the owner
|
||
|
// could ignore).
|
||
|
if (set_x11_selection_owner())
|
||
|
return;
|
||
|
|
||
|
// Clear the clipboard data from the selection owner
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
if (m_window != owner) {
|
||
|
xcb_selection_clear_event_t event;
|
||
|
event.response_type = XCB_SELECTION_CLEAR;
|
||
|
event.pad0 = 0;
|
||
|
event.sequence = 0;
|
||
|
event.time = XCB_CURRENT_TIME;
|
||
|
event.owner = owner;
|
||
|
event.selection = get_atom(CLIPBOARD);
|
||
|
|
||
|
xcb_send_event(m_connection, false,
|
||
|
owner,
|
||
|
XCB_EVENT_MASK_NO_EVENT,
|
||
|
(const char*)&event);
|
||
|
|
||
|
xcb_flush(m_connection);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bool is_convertible(format f) const {
|
||
|
const atoms atoms = get_format_atoms(f);
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
|
||
|
// If we are the owner, we just can check the m_data map
|
||
|
if (owner == m_window) {
|
||
|
for (xcb_atom_t atom : atoms) {
|
||
|
auto it = m_data.find(atom);
|
||
|
if (it != m_data.end())
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
// Ask to the selection owner the available formats/atoms/targets.
|
||
|
else if (owner) {
|
||
|
return
|
||
|
get_data_from_selection_owner(
|
||
|
{ get_atom(TARGETS) },
|
||
|
[this, &atoms]() -> bool {
|
||
|
assert(m_reply_data);
|
||
|
if (!m_reply_data)
|
||
|
return false;
|
||
|
|
||
|
const xcb_atom_t* sel_atoms = (const xcb_atom_t*)&(*m_reply_data)[0];
|
||
|
int sel_natoms = m_reply_data->size() / sizeof(xcb_atom_t);
|
||
|
auto atoms_begin = atoms.begin();
|
||
|
auto atoms_end = atoms.end();
|
||
|
for (int i=0; i<sel_natoms; ++i) {
|
||
|
if (std::find(atoms_begin,
|
||
|
atoms_end,
|
||
|
sel_atoms[i]) != atoms_end) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool set_data(format f, const char* buf, size_t len) {
|
||
|
if (!set_x11_selection_owner())
|
||
|
return false;
|
||
|
|
||
|
const atoms atoms = get_format_atoms(f);
|
||
|
if (atoms.empty())
|
||
|
return false;
|
||
|
|
||
|
buffer_ptr shared_data_buf = std::make_shared<std::vector<uint8_t>>(len);
|
||
|
std::copy(buf,
|
||
|
buf+len,
|
||
|
shared_data_buf->begin());
|
||
|
for (xcb_atom_t atom : atoms)
|
||
|
m_data[atom] = shared_data_buf;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool get_data(format f, char* buf, size_t len) const {
|
||
|
const atoms atoms = get_format_atoms(f);
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
if (owner == m_window) {
|
||
|
for (xcb_atom_t atom : atoms) {
|
||
|
auto it = m_data.find(atom);
|
||
|
if (it != m_data.end()) {
|
||
|
size_t n = std::min(len, it->second->size());
|
||
|
std::copy(it->second->begin(),
|
||
|
it->second->begin()+n,
|
||
|
buf);
|
||
|
|
||
|
if (f == text_format()) {
|
||
|
// Add an extra null char
|
||
|
if (n < len)
|
||
|
buf[n] = 0;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (owner) {
|
||
|
if (get_data_from_selection_owner(
|
||
|
atoms,
|
||
|
[this, buf, len, f]() -> bool {
|
||
|
size_t n = std::min(len, m_reply_data->size());
|
||
|
std::copy(m_reply_data->begin(),
|
||
|
m_reply_data->begin()+n,
|
||
|
buf);
|
||
|
|
||
|
if (f == text_format()) {
|
||
|
if (n < len)
|
||
|
buf[n] = 0; // Include a null character
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
})) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
size_t get_data_length(format f) const {
|
||
|
size_t len = 0;
|
||
|
const atoms atoms = get_format_atoms(f);
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
if (owner == m_window) {
|
||
|
for (xcb_atom_t atom : atoms) {
|
||
|
auto it = m_data.find(atom);
|
||
|
if (it != m_data.end()) {
|
||
|
len = it->second->size();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (owner) {
|
||
|
if (!get_data_from_selection_owner(
|
||
|
atoms,
|
||
|
[this, &len]() -> bool {
|
||
|
len = m_reply_data->size();
|
||
|
return true;
|
||
|
})) {
|
||
|
// Error getting data length
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
if (f == text_format() && len > 0) {
|
||
|
++len; // Add an extra byte for the null char
|
||
|
}
|
||
|
return len;
|
||
|
}
|
||
|
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
|
||
|
bool set_image(const image& image) {
|
||
|
if (!set_x11_selection_owner())
|
||
|
return false;
|
||
|
|
||
|
m_image = image;
|
||
|
|
||
|
#ifdef HAVE_PNG_H
|
||
|
// Put a nullptr in the m_data for image/png format and then we'll
|
||
|
// encode the png data when the image is requested in this format.
|
||
|
m_data[get_atom(MIME_IMAGE_PNG)] = buffer_ptr();
|
||
|
#endif
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool get_image(image& output_img) const {
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
if (owner == m_window) {
|
||
|
if (m_image.is_valid()) {
|
||
|
output_img = m_image;
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
#ifdef HAVE_PNG_H
|
||
|
else if (owner &&
|
||
|
get_data_from_selection_owner(
|
||
|
{ get_atom(MIME_IMAGE_PNG) },
|
||
|
[this, &output_img]() -> bool {
|
||
|
return x11::read_png(&(*m_reply_data)[0],
|
||
|
m_reply_data->size(),
|
||
|
&output_img, nullptr);
|
||
|
})) {
|
||
|
return true;
|
||
|
}
|
||
|
#endif
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool get_image_spec(image_spec& spec) const {
|
||
|
const xcb_window_t owner = get_x11_selection_owner();
|
||
|
if (owner == m_window) {
|
||
|
if (m_image.is_valid()) {
|
||
|
spec = m_image.spec();
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
#ifdef HAVE_PNG_H
|
||
|
else if (owner &&
|
||
|
get_data_from_selection_owner(
|
||
|
{ get_atom(MIME_IMAGE_PNG) },
|
||
|
[this, &spec]() -> bool {
|
||
|
return x11::read_png(&(*m_reply_data)[0],
|
||
|
m_reply_data->size(),
|
||
|
nullptr, &spec);
|
||
|
})) {
|
||
|
return true;
|
||
|
}
|
||
|
#endif
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
#endif // CLIP_ENABLE_IMAGE
|
||
|
|
||
|
format register_format(const std::string& name) {
|
||
|
xcb_atom_t atom = get_atom(name.c_str());
|
||
|
m_custom_formats.push_back(atom);
|
||
|
return (format)(m_custom_formats.size()-1) + kBaseForCustomFormats;
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
|
||
|
void process_x11_events() {
|
||
|
bool stop = false;
|
||
|
xcb_generic_event_t* event;
|
||
|
while (!stop && (event = xcb_wait_for_event(m_connection))) {
|
||
|
int type = (event->response_type & ~0x80);
|
||
|
|
||
|
switch (type) {
|
||
|
|
||
|
case XCB_DESTROY_NOTIFY:
|
||
|
// To stop the message loop we can just destroy the window
|
||
|
stop = true;
|
||
|
break;
|
||
|
|
||
|
// Someone else has new content in the clipboard, so is
|
||
|
// notifying us that we should delete our data now.
|
||
|
case XCB_SELECTION_CLEAR:
|
||
|
handle_selection_clear_event(
|
||
|
(xcb_selection_clear_event_t*)event);
|
||
|
break;
|
||
|
|
||
|
// Someone is requesting the clipboard content from us.
|
||
|
case XCB_SELECTION_REQUEST:
|
||
|
handle_selection_request_event(
|
||
|
(xcb_selection_request_event_t*)event);
|
||
|
break;
|
||
|
|
||
|
// We've requested the clipboard content and this is the
|
||
|
// answer.
|
||
|
case XCB_SELECTION_NOTIFY:
|
||
|
handle_selection_notify_event(
|
||
|
(xcb_selection_notify_event_t*)event);
|
||
|
break;
|
||
|
|
||
|
case XCB_PROPERTY_NOTIFY:
|
||
|
handle_property_notify_event(
|
||
|
(xcb_property_notify_event_t*)event);
|
||
|
break;
|
||
|
|
||
|
}
|
||
|
|
||
|
free(event);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void handle_selection_clear_event(xcb_selection_clear_event_t* event) {
|
||
|
if (event->selection == get_atom(CLIPBOARD)) {
|
||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||
|
clear_data(); // Clear our clipboard data
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void handle_selection_request_event(xcb_selection_request_event_t* event) {
|
||
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||
|
|
||
|
if (event->target == get_atom(TARGETS)) {
|
||
|
atoms targets;
|
||
|
targets.push_back(get_atom(TARGETS));
|
||
|
#ifdef CLIP_SUPPORT_SAVE_TARGETS
|
||
|
targets.push_back(get_atom(SAVE_TARGETS));
|
||
|
targets.push_back(get_atom(MULTIPLE));
|
||
|
#endif
|
||
|
for (const auto& it : m_data)
|
||
|
targets.push_back(it.first);
|
||
|
|
||
|
// Set the "property" of "requestor" with the clipboard
|
||
|
// formats ("targets", atoms) that we provide.
|
||
|
xcb_change_property(
|
||
|
m_connection,
|
||
|
XCB_PROP_MODE_REPLACE,
|
||
|
event->requestor,
|
||
|
event->property,
|
||
|
get_atom(ATOM),
|
||
|
8*sizeof(xcb_atom_t),
|
||
|
targets.size(),
|
||
|
&targets[0]);
|
||
|
}
|
||
|
#ifdef CLIP_SUPPORT_SAVE_TARGETS
|
||
|
else if (event->target == get_atom(SAVE_TARGETS)) {
|
||
|
// Do nothing
|
||
|
}
|
||
|
else if (event->target == get_atom(MULTIPLE)) {
|
||
|
xcb_get_property_reply_t* reply =
|
||
|
get_and_delete_property(event->requestor,
|
||
|
event->property,
|
||
|
get_atom(ATOM_PAIR),
|
||
|
false);
|
||
|
if (reply) {
|
||
|
for (xcb_atom_t
|
||
|
*ptr=(xcb_atom_t*)xcb_get_property_value(reply),
|
||
|
*end=ptr + (xcb_get_property_value_length(reply)/sizeof(xcb_atom_t));
|
||
|
ptr<end; ) {
|
||
|
xcb_atom_t target = *ptr++;
|
||
|
xcb_atom_t property = *ptr++;
|
||
|
|
||
|
if (!set_requestor_property_with_clipboard_content(
|
||
|
event->requestor,
|
||
|
property,
|
||
|
target)) {
|
||
|
xcb_change_property(
|
||
|
m_connection,
|
||
|
XCB_PROP_MODE_REPLACE,
|
||
|
event->requestor,
|
||
|
event->property,
|
||
|
XCB_ATOM_NONE, 0, 0, nullptr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
free(reply);
|
||
|
}
|
||
|
}
|
||
|
#endif // CLIP_SUPPORT_SAVE_TARGETS
|
||
|
else {
|
||
|
if (!set_requestor_property_with_clipboard_content(
|
||
|
event->requestor,
|
||
|
event->property,
|
||
|
event->target)) {
|
||
|
// If the requested "target" type is not present in our
|
||
|
// clipboard, we continue normally sending a SelectionNotify
|
||
|
// to the "requestor" anyway because some text editors
|
||
|
// (e.g. Emacs) request the TIMESTAMP target (without asking
|
||
|
// if it's present in TARGETS) after asking for UTF8_STRING.
|
||
|
//
|
||
|
// Sending the SelectionNotify will wake up the "requestor"
|
||
|
// that is asking for the clipboard content. In this way we
|
||
|
// avoid a "Timed out waiting for reply from selection owner"
|
||
|
// error in Emacs (and probably other text editors).
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Notify the "requestor" that we've already updated the property.
|
||
|
xcb_selection_notify_event_t notify;
|
||
|
notify.response_type = XCB_SELECTION_NOTIFY;
|
||
|
notify.pad0 = 0;
|
||
|
notify.sequence = 0;
|
||
|
notify.time = event->time;
|
||
|
notify.requestor = event->requestor;
|
||
|
notify.selection = event->selection;
|
||
|
notify.target = event->target;
|
||
|
notify.property = event->property;
|
||
|
|
||
|
xcb_send_event(m_connection, false,
|
||
|
event->requestor,
|
||
|
XCB_EVENT_MASK_NO_EVENT, // SelectionNotify events go without mask
|
||
|
(const char*)¬ify);
|
||
|
|
||
|
xcb_flush(m_connection);
|
||
|
}
|
||
|
|
||
|
bool set_requestor_property_with_clipboard_content(const xcb_atom_t requestor,
|
||
|
const xcb_atom_t property,
|
||
|
const xcb_atom_t target) {
|
||
|
auto it = m_data.find(target);
|
||
|
if (it == m_data.end()) {
|
||
|
// Nothing to do (unsupported target)
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// This can be null of the data was set from an image but we
|
||
|
// didn't encode the image yet (e.g. to image/png format).
|
||
|
if (!it->second) {
|
||
|
encode_data_on_demand(*it);
|
||
|
|
||
|
// Return nothing, the given "target" cannot be constructed
|
||
|
// (maybe by some encoding error).
|
||
|
if (!it->second)
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Set the "property" of "requestor" with the
|
||
|
// clipboard content in the requested format ("target").
|
||
|
xcb_change_property(
|
||
|
m_connection,
|
||
|
XCB_PROP_MODE_REPLACE,
|
||
|
requestor,
|
||
|
property,
|
||
|
target,
|
||
|
8,
|
||
|
it->second->size(),
|
||
|
&(*it->second)[0]);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void handle_selection_notify_event(xcb_selection_notify_event_t* event) {
|
||
|
assert(event->requestor == m_window);
|
||
|
|
||
|
if (event->target == get_atom(TARGETS))
|
||
|
m_target_atom = get_atom(ATOM);
|
||
|
else
|
||
|
m_target_atom = event->target;
|
||
|
|
||
|
xcb_get_property_reply_t* reply =
|
||
|
get_and_delete_property(event->requestor,
|
||
|
event->property,
|
||
|
m_target_atom);
|
||
|
if (reply) {
|
||
|
// In this case, We're going to receive the clipboard content in
|
||
|
// chunks of data with several PropertyNotify events.
|
||
|
if (reply->type == get_atom(INCR)) {
|
||
|
free(reply);
|
||
|
|
||
|
reply = get_and_delete_property(event->requestor,
|
||
|
event->property,
|
||
|
get_atom(INCR));
|
||
|
if (reply) {
|
||
|
if (xcb_get_property_value_length(reply) == 4) {
|
||
|
uint32_t n = *(uint32_t*)xcb_get_property_value(reply);
|
||
|
m_reply_data = std::make_shared<std::vector<uint8_t>>(n);
|
||
|
m_reply_offset = 0;
|
||
|
m_incr_process = true;
|
||
|
m_incr_received = true;
|
||
|
}
|
||
|
free(reply);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
// Simple case, the whole clipboard content in just one reply
|
||
|
// (without the INCR method).
|
||
|
m_reply_data.reset();
|
||
|
m_reply_offset = 0;
|
||
|
copy_reply_data(reply);
|
||
|
|
||
|
call_callback(reply);
|
||
|
|
||
|
free(reply);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void handle_property_notify_event(xcb_property_notify_event_t* event) {
|
||
|
if (m_incr_process &&
|
||
|
event->state == XCB_PROPERTY_NEW_VALUE &&
|
||
|
event->atom == get_atom(CLIPBOARD)) {
|
||
|
xcb_get_property_reply_t* reply =
|
||
|
get_and_delete_property(event->window,
|
||
|
event->atom,
|
||
|
m_target_atom);
|
||
|
if (reply) {
|
||
|
m_incr_received = true;
|
||
|
|
||
|
// When the length is 0 it means that the content was
|
||
|
// completely sent by the selection owner.
|
||
|
if (xcb_get_property_value_length(reply) > 0) {
|
||
|
copy_reply_data(reply);
|
||
|
}
|
||
|
else {
|
||
|
// Now that m_reply_data has the complete clipboard content,
|
||
|
// we can call the m_callback.
|
||
|
call_callback(reply);
|
||
|
m_incr_process = false;
|
||
|
}
|
||
|
free(reply);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
xcb_get_property_reply_t* get_and_delete_property(xcb_window_t window,
|
||
|
xcb_atom_t property,
|
||
|
xcb_atom_t atom,
|
||
|
bool delete_prop = true) {
|
||
|
xcb_get_property_cookie_t cookie =
|
||
|
xcb_get_property(m_connection,
|
||
|
delete_prop,
|
||
|
window,
|
||
|
property,
|
||
|
atom,
|
||
|
0, 0x1fffffff); // 0x1fffffff = INT32_MAX / 4
|
||
|
|
||
|
xcb_generic_error_t* err = nullptr;
|
||
|
xcb_get_property_reply_t* reply =
|
||
|
xcb_get_property_reply(m_connection, cookie, &err);
|
||
|
if (err) {
|
||
|
// TODO report error
|
||
|
free(err);
|
||
|
}
|
||
|
return reply;
|
||
|
}
|
||
|
|
||
|
// Concatenates the new data received in "reply" into "m_reply_data"
|
||
|
// buffer.
|
||
|
void copy_reply_data(xcb_get_property_reply_t* reply) {
|
||
|
const uint8_t* src = (const uint8_t*)xcb_get_property_value(reply);
|
||
|
// n = length of "src" in bytes
|
||
|
size_t n = xcb_get_property_value_length(reply);
|
||
|
|
||
|
size_t req = m_reply_offset+n;
|
||
|
if (!m_reply_data) {
|
||
|
m_reply_data = std::make_shared<std::vector<uint8_t>>(req);
|
||
|
}
|
||
|
// The "m_reply_data" size can be smaller because the size
|
||
|
// specified in INCR property is just a lower bound.
|
||
|
else if (req > m_reply_data->size()) {
|
||
|
m_reply_data->resize(req);
|
||
|
}
|
||
|
|
||
|
std::copy(src, src+n, m_reply_data->begin()+m_reply_offset);
|
||
|
m_reply_offset += n;
|
||
|
}
|
||
|
|
||
|
// Calls the current m_callback() to handle the clipboard content
|
||
|
// received from the owner.
|
||
|
void call_callback(xcb_get_property_reply_t* reply) {
|
||
|
m_callback_result = false;
|
||
|
if (m_callback)
|
||
|
m_callback_result = m_callback();
|
||
|
|
||
|
m_cv.notify_one();
|
||
|
|
||
|
m_reply_data.reset();
|
||
|
}
|
||
|
|
||
|
bool get_data_from_selection_owner(const atoms& atoms,
|
||
|
const notify_callback&& callback,
|
||
|
xcb_atom_t selection = 0) const {
|
||
|
if (!selection)
|
||
|
selection = get_atom(CLIPBOARD);
|
||
|
|
||
|
// Put the callback on "m_callback" so we can call it on
|
||
|
// SelectionNotify event.
|
||
|
m_callback = std::move(callback);
|
||
|
|
||
|
// Clear data if we are not the selection owner.
|
||
|
if (m_window != get_x11_selection_owner())
|
||
|
m_data.clear();
|
||
|
|
||
|
// Ask to the selection owner for its content on each known
|
||
|
// text format/atom.
|
||
|
for (xcb_atom_t atom : atoms) {
|
||
|
xcb_convert_selection(m_connection,
|
||
|
m_window, // Send us the result
|
||
|
selection, // Clipboard selection
|
||
|
atom, // The clipboard format that we're requesting
|
||
|
get_atom(CLIPBOARD), // Leave result in this window's property
|
||
|
XCB_CURRENT_TIME);
|
||
|
|
||
|
xcb_flush(m_connection);
|
||
|
|
||
|
// We use the "m_incr_received" to wait several timeouts in case
|
||
|
// that we've received the INCR SelectionNotify or
|
||
|
// PropertyNotify events.
|
||
|
do {
|
||
|
m_incr_received = false;
|
||
|
|
||
|
// Wait a response for 100 milliseconds
|
||
|
std::cv_status status =
|
||
|
m_cv.wait_for(m_lock,
|
||
|
std::chrono::milliseconds(get_x11_wait_timeout()));
|
||
|
if (status == std::cv_status::no_timeout) {
|
||
|
// If the condition variable was notified, it means that the
|
||
|
// callback was called correctly.
|
||
|
return m_callback_result;
|
||
|
}
|
||
|
} while (m_incr_received);
|
||
|
}
|
||
|
|
||
|
// Reset callback
|
||
|
m_callback = notify_callback();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
atoms get_atoms(const char** names,
|
||
|
const int n) const {
|
||
|
atoms result(n, 0);
|
||
|
std::vector<xcb_intern_atom_cookie_t> cookies(n);
|
||
|
|
||
|
for (int i=0; i<n; ++i) {
|
||
|
auto it = m_atoms.find(names[i]);
|
||
|
if (it != m_atoms.end())
|
||
|
result[i] = it->second;
|
||
|
else
|
||
|
cookies[i] = xcb_intern_atom(
|
||
|
m_connection, 0,
|
||
|
std::strlen(names[i]), names[i]);
|
||
|
}
|
||
|
|
||
|
for (int i=0; i<n; ++i) {
|
||
|
if (result[i] == 0) {
|
||
|
xcb_intern_atom_reply_t* reply =
|
||
|
xcb_intern_atom_reply(m_connection,
|
||
|
cookies[i],
|
||
|
nullptr);
|
||
|
if (reply) {
|
||
|
result[i] = m_atoms[names[i]] = reply->atom;
|
||
|
free(reply);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
xcb_atom_t get_atom(const char* name) const {
|
||
|
auto it = m_atoms.find(name);
|
||
|
if (it != m_atoms.end())
|
||
|
return it->second;
|
||
|
|
||
|
xcb_atom_t result = 0;
|
||
|
xcb_intern_atom_cookie_t cookie =
|
||
|
xcb_intern_atom(m_connection, 0,
|
||
|
std::strlen(name), name);
|
||
|
|
||
|
xcb_intern_atom_reply_t* reply =
|
||
|
xcb_intern_atom_reply(m_connection,
|
||
|
cookie,
|
||
|
nullptr);
|
||
|
if (reply) {
|
||
|
result = m_atoms[name] = reply->atom;
|
||
|
free(reply);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
xcb_atom_t get_atom(CommonAtom i) const {
|
||
|
if (m_common_atoms.empty()) {
|
||
|
m_common_atoms =
|
||
|
get_atoms(kCommonAtomNames,
|
||
|
sizeof(kCommonAtomNames) / sizeof(kCommonAtomNames[0]));
|
||
|
}
|
||
|
return m_common_atoms[i];
|
||
|
}
|
||
|
|
||
|
const atoms& get_text_format_atoms() const {
|
||
|
if (m_text_atoms.empty()) {
|
||
|
const char* names[] = {
|
||
|
// Prefer utf-8 formats first
|
||
|
"UTF8_STRING",
|
||
|
"text/plain;charset=utf-8",
|
||
|
"text/plain;charset=UTF-8",
|
||
|
"GTK_TEXT_BUFFER_CONTENTS", // Required for gedit (and maybe gtk+ apps)
|
||
|
// ANSI C strings?
|
||
|
"STRING",
|
||
|
"TEXT",
|
||
|
"text/plain",
|
||
|
};
|
||
|
m_text_atoms = get_atoms(names, sizeof(names) / sizeof(names[0]));
|
||
|
}
|
||
|
return m_text_atoms;
|
||
|
}
|
||
|
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
|
||
|
const atoms& get_image_format_atoms() const {
|
||
|
if (m_image_atoms.empty()) {
|
||
|
#ifdef HAVE_PNG_H
|
||
|
m_image_atoms.push_back(get_atom(MIME_IMAGE_PNG));
|
||
|
#endif
|
||
|
}
|
||
|
return m_image_atoms;
|
||
|
}
|
||
|
|
||
|
#endif // CLIP_ENABLE_IMAGE
|
||
|
|
||
|
atoms get_format_atoms(const format f) const {
|
||
|
atoms atoms;
|
||
|
if (f == text_format()) {
|
||
|
atoms = get_text_format_atoms();
|
||
|
}
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
else if (f == image_format()) {
|
||
|
atoms = get_image_format_atoms();
|
||
|
}
|
||
|
#endif // CLIP_ENABLE_IMAGE
|
||
|
else {
|
||
|
xcb_atom_t atom = get_format_atom(f);
|
||
|
if (atom)
|
||
|
atoms.push_back(atom);
|
||
|
}
|
||
|
return atoms;
|
||
|
}
|
||
|
|
||
|
#if !defined(NDEBUG)
|
||
|
// This can be used to print debugging messages.
|
||
|
std::string get_atom_name(xcb_atom_t atom) const {
|
||
|
std::string result;
|
||
|
xcb_get_atom_name_cookie_t cookie =
|
||
|
xcb_get_atom_name(m_connection, atom);
|
||
|
xcb_generic_error_t* err = nullptr;
|
||
|
xcb_get_atom_name_reply_t* reply =
|
||
|
xcb_get_atom_name_reply(m_connection, cookie, &err);
|
||
|
if (err) {
|
||
|
free(err);
|
||
|
}
|
||
|
if (reply) {
|
||
|
int len = xcb_get_atom_name_name_length(reply);
|
||
|
if (len > 0) {
|
||
|
result.resize(len);
|
||
|
char* name = xcb_get_atom_name_name(reply);
|
||
|
if (name)
|
||
|
std::copy(name, name+len, result.begin());
|
||
|
}
|
||
|
free(reply);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
bool set_x11_selection_owner() const {
|
||
|
xcb_void_cookie_t cookie =
|
||
|
xcb_set_selection_owner_checked(m_connection,
|
||
|
m_window,
|
||
|
get_atom(CLIPBOARD),
|
||
|
XCB_CURRENT_TIME);
|
||
|
xcb_generic_error_t* err =
|
||
|
xcb_request_check(m_connection,
|
||
|
cookie);
|
||
|
if (err) {
|
||
|
free(err);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
xcb_window_t get_x11_selection_owner() const {
|
||
|
xcb_window_t result = 0;
|
||
|
xcb_get_selection_owner_cookie_t cookie =
|
||
|
xcb_get_selection_owner(m_connection,
|
||
|
get_atom(CLIPBOARD));
|
||
|
|
||
|
xcb_get_selection_owner_reply_t* reply =
|
||
|
xcb_get_selection_owner_reply(m_connection, cookie, nullptr);
|
||
|
if (reply) {
|
||
|
result = reply->owner;
|
||
|
free(reply);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
xcb_atom_t get_format_atom(const format f) const {
|
||
|
int i = f - kBaseForCustomFormats;
|
||
|
if (i >= 0 && i < int(m_custom_formats.size()))
|
||
|
return m_custom_formats[i];
|
||
|
else
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
void encode_data_on_demand(std::pair<const xcb_atom_t, buffer_ptr>& e) {
|
||
|
#if defined(CLIP_ENABLE_IMAGE) && defined(HAVE_PNG_H)
|
||
|
if (e.first == get_atom(MIME_IMAGE_PNG)) {
|
||
|
assert(m_image.is_valid());
|
||
|
if (!m_image.is_valid())
|
||
|
return;
|
||
|
|
||
|
std::vector<uint8_t> output;
|
||
|
if (x11::write_png(m_image, output)) {
|
||
|
e.second =
|
||
|
std::make_shared<std::vector<uint8_t>>(
|
||
|
std::move(output));
|
||
|
}
|
||
|
// else { TODO report png conversion errors }
|
||
|
}
|
||
|
#endif // defined(CLIP_ENABLE_IMAGE) && defined(HAVE_PNG_H)
|
||
|
}
|
||
|
|
||
|
// Access to the whole Manager
|
||
|
std::mutex m_mutex;
|
||
|
|
||
|
// Lock used in the main thread using the Manager (i.e. by lock::impl)
|
||
|
mutable std::unique_lock<std::mutex> m_lock;
|
||
|
|
||
|
// Connection to X11 server
|
||
|
xcb_connection_t* m_connection;
|
||
|
|
||
|
// Temporal background window used to own the clipboard and process
|
||
|
// all events related about the clipboard in a background thread
|
||
|
xcb_window_t m_window;
|
||
|
|
||
|
// Used to wait/notify the arrival of the SelectionNotify event when
|
||
|
// we requested the clipboard content from other selection owner.
|
||
|
mutable std::condition_variable m_cv;
|
||
|
|
||
|
// Thread used to run a background message loop to wait X11 events
|
||
|
// about clipboard. The X11 selection owner will be a hidden window
|
||
|
// created by us just for the clipboard purpose/communication.
|
||
|
std::thread m_thread;
|
||
|
|
||
|
// Internal callback used when a SelectionNotify is received (or the
|
||
|
// whole data content is received by the INCR method). So this
|
||
|
// callback can use the notification by different purposes (e.g. get
|
||
|
// the data length only, or get/process the data content, etc.).
|
||
|
mutable notify_callback m_callback;
|
||
|
|
||
|
// Result returned by the m_callback. Used as return value in the
|
||
|
// get_data_from_selection_owner() function. For example, if the
|
||
|
// callback must read a "image/png" file from the clipboard data and
|
||
|
// fails, the callback can return false and finally the get_image()
|
||
|
// will return false (i.e. there is data, but it's not a valid image
|
||
|
// format).
|
||
|
std::atomic<bool> m_callback_result;
|
||
|
|
||
|
// Cache of known atoms
|
||
|
mutable std::map<std::string, xcb_atom_t> m_atoms;
|
||
|
|
||
|
// Cache of common used atoms by us
|
||
|
mutable atoms m_common_atoms;
|
||
|
|
||
|
// Cache of atoms related to text or image content
|
||
|
mutable atoms m_text_atoms;
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
mutable atoms m_image_atoms;
|
||
|
#endif
|
||
|
|
||
|
// Actual clipboard data generated by us (when we "copy" content in
|
||
|
// the clipboard, it means that we own the X11 "CLIPBOARD"
|
||
|
// selection, and in case of SelectionRequest events, we've to
|
||
|
// return the data stored in this "m_data" field)
|
||
|
mutable std::map<xcb_atom_t, buffer_ptr> m_data;
|
||
|
|
||
|
// Copied image in the clipboard. As we have to transfer the image
|
||
|
// in some specific format (e.g. image/png) we want to keep a copy
|
||
|
// of the image and make the conversion when the clipboard data is
|
||
|
// requested by other process.
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
mutable image m_image;
|
||
|
#endif
|
||
|
|
||
|
// True if we have received an INCR notification so we're going to
|
||
|
// process several PropertyNotify to concatenate all data chunks.
|
||
|
bool m_incr_process;
|
||
|
|
||
|
// Variable used to wait more time if we've received an INCR
|
||
|
// notification, which means that we're going to receive large
|
||
|
// amounts of data from the selection owner.
|
||
|
mutable bool m_incr_received;
|
||
|
|
||
|
// Target/selection format used in the SelectionNotify. Used in the
|
||
|
// INCR method to get data from the same property in the same format
|
||
|
// (target) on each PropertyNotify.
|
||
|
xcb_atom_t m_target_atom;
|
||
|
|
||
|
// Each time we receive data from the selection owner, we put that
|
||
|
// data in this buffer. If we get the data with the INCR method,
|
||
|
// we'll concatenate chunks of data in this buffer to complete the
|
||
|
// whole clipboard content.
|
||
|
buffer_ptr m_reply_data;
|
||
|
|
||
|
// Used to concatenate chunks of data in "m_reply_data" from several
|
||
|
// PropertyNotify when we are getting the selection owner data with
|
||
|
// the INCR method.
|
||
|
size_t m_reply_offset;
|
||
|
|
||
|
// List of user-defined formats/atoms.
|
||
|
std::vector<xcb_atom_t> m_custom_formats;
|
||
|
};
|
||
|
|
||
|
Manager* manager = nullptr;
|
||
|
|
||
|
void delete_manager_atexit() {
|
||
|
if (manager) {
|
||
|
delete manager;
|
||
|
manager = nullptr;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Manager* get_manager() {
|
||
|
if (!manager) {
|
||
|
manager = new Manager;
|
||
|
std::atexit(delete_manager_atexit);
|
||
|
}
|
||
|
return manager;
|
||
|
}
|
||
|
|
||
|
} // anonymous namespace
|
||
|
|
||
|
lock::impl::impl(void*) : m_locked(false) {
|
||
|
m_locked = get_manager()->try_lock();
|
||
|
}
|
||
|
|
||
|
lock::impl::~impl() {
|
||
|
if (m_locked)
|
||
|
manager->unlock();
|
||
|
}
|
||
|
|
||
|
bool lock::impl::clear() {
|
||
|
manager->clear();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool lock::impl::is_convertible(format f) const {
|
||
|
return manager->is_convertible(f);
|
||
|
}
|
||
|
|
||
|
bool lock::impl::set_data(format f, const char* buf, size_t len) {
|
||
|
return manager->set_data(f, buf, len);
|
||
|
}
|
||
|
|
||
|
bool lock::impl::get_data(format f, char* buf, size_t len) const {
|
||
|
return manager->get_data(f, buf, len);
|
||
|
}
|
||
|
|
||
|
size_t lock::impl::get_data_length(format f) const {
|
||
|
return manager->get_data_length(f);
|
||
|
}
|
||
|
|
||
|
#if CLIP_ENABLE_IMAGE
|
||
|
|
||
|
bool lock::impl::set_image(const image& image) {
|
||
|
return manager->set_image(image);
|
||
|
}
|
||
|
|
||
|
bool lock::impl::get_image(image& output_img) const {
|
||
|
return manager->get_image(output_img);
|
||
|
}
|
||
|
|
||
|
bool lock::impl::get_image_spec(image_spec& spec) const {
|
||
|
return manager->get_image_spec(spec);
|
||
|
}
|
||
|
|
||
|
#endif // CLIP_ENABLE_IMAGE
|
||
|
|
||
|
format register_format(const std::string& name) {
|
||
|
return get_manager()->register_format(name);
|
||
|
}
|
||
|
|
||
|
} // namespace clip
|