Fix issues with Linux clipboard

Backport for X11 Display Server fixes on 3.2 branch.

1. Implement SAVE_TARGETS mechanism

Allows sending the clipboard content to the clipboard manager on exit to
keep the content when using a clipboard manager that doesn't
automatically makes a backup when copying.

MULTIPLE selection mechanism also had to be implemented, because in this
case, the clipboard manager might request multiple selection targets at
once.

Known use case: Ubuntu with XFCE4

2. Implement INCR mechanism

Allows pasting from x11 clipboard to receive data incrementally, which
is required when handling data size > 256KB.
This commit is contained in:
PouleyKetchoupp 2020-10-09 21:19:51 +02:00
parent 0cea2731f7
commit de4a1d09b0
2 changed files with 282 additions and 79 deletions

View File

@ -813,7 +813,7 @@ void OS_X11::finalize() {
events_thread_done = true; events_thread_done = true;
Thread::wait_to_finish(events_thread); Thread::wait_to_finish(events_thread);
memdelete(events_thread); memdelete(events_thread);
events_thread = nullptr; events_thread = NULL;
if (main_loop) if (main_loop)
memdelete(main_loop); memdelete(main_loop);
@ -2009,53 +2009,106 @@ void OS_X11::_handle_key_event(XKeyEvent *p_event, LocalVector<XEvent> &p_events
input->accumulate_input_event(k); input->accumulate_input_event(k);
} }
void OS_X11::_handle_selection_request_event(XSelectionRequestEvent *p_event) { Atom OS_X11::_process_selection_request_target(Atom p_target, Window p_requestor, Atom p_property) const {
XEvent respond; if (p_target == XInternAtom(x11_display, "TARGETS", 0)) {
if (p_event->target == XInternAtom(x11_display, "UTF8_STRING", 0) || // Request to list all supported targets.
p_event->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || Atom data[9];
p_event->target == XInternAtom(x11_display, "TEXT", 0) ||
p_event->target == XA_STRING ||
p_event->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) ||
p_event->target == XInternAtom(x11_display, "text/plain", 0)) {
// Directly using internal clipboard because we know our window
// is the owner during a selection request.
CharString clip = OS::get_clipboard().utf8();
XChangeProperty(x11_display,
p_event->requestor,
p_event->property,
p_event->target,
8,
PropModeReplace,
(unsigned char *)clip.get_data(),
clip.length());
respond.xselection.property = p_event->property;
} else if (p_event->target == XInternAtom(x11_display, "TARGETS", 0)) {
Atom data[7];
data[0] = XInternAtom(x11_display, "TARGETS", 0); data[0] = XInternAtom(x11_display, "TARGETS", 0);
data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); data[1] = XInternAtom(x11_display, "SAVE_TARGETS", 0);
data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); data[2] = XInternAtom(x11_display, "MULTIPLE", 0);
data[3] = XInternAtom(x11_display, "TEXT", 0); data[3] = XInternAtom(x11_display, "UTF8_STRING", 0);
data[4] = XA_STRING; data[4] = XInternAtom(x11_display, "COMPOUND_TEXT", 0);
data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); data[5] = XInternAtom(x11_display, "TEXT", 0);
data[6] = XInternAtom(x11_display, "text/plain", 0); data[6] = XA_STRING;
data[7] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0);
data[8] = XInternAtom(x11_display, "text/plain", 0);
XChangeProperty(x11_display, XChangeProperty(x11_display,
p_event->requestor, p_requestor,
p_event->property, p_property,
XA_ATOM, XA_ATOM,
32, 32,
PropModeReplace, PropModeReplace,
(unsigned char *)&data, (unsigned char *)&data,
sizeof(data) / sizeof(data[0])); sizeof(data) / sizeof(data[0]));
respond.xselection.property = p_event->property;
return p_property;
} else if (p_target == XInternAtom(x11_display, "SAVE_TARGETS", 0)) {
// Request to check if SAVE_TARGETS is supported, nothing special to do.
XChangeProperty(x11_display,
p_requestor,
p_property,
XInternAtom(x11_display, "NULL", False),
32,
PropModeReplace,
NULL,
0);
return p_property;
} else if (p_target == XInternAtom(x11_display, "UTF8_STRING", 0) ||
p_target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) ||
p_target == XInternAtom(x11_display, "TEXT", 0) ||
p_target == XA_STRING ||
p_target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) ||
p_target == XInternAtom(x11_display, "text/plain", 0)) {
// Directly using internal clipboard because we know our window
// is the owner during a selection request.
CharString clip = OS::get_clipboard().utf8();
XChangeProperty(x11_display,
p_requestor,
p_property,
p_target,
8,
PropModeReplace,
(unsigned char *)clip.get_data(),
clip.length());
return p_property;
} else { } else {
char *targetname = XGetAtomName(x11_display, p_event->target); char *target_name = XGetAtomName(x11_display, p_target);
printf("No Target '%s'\n", targetname); printf("Target '%s' not supported.\n", target_name);
if (targetname) { if (target_name) {
XFree(targetname); XFree(target_name);
} }
return None;
}
}
void OS_X11::_handle_selection_request_event(XSelectionRequestEvent *p_event) const {
XEvent respond;
if (p_event->target == XInternAtom(x11_display, "MULTIPLE", 0)) {
// Request for multiple target conversions at once.
Atom atom_pair = XInternAtom(x11_display, "ATOM_PAIR", False);
respond.xselection.property = None; respond.xselection.property = None;
Atom type;
int format;
unsigned long len;
unsigned long remaining;
unsigned char *data = NULL;
if (XGetWindowProperty(x11_display, p_event->requestor, p_event->property, 0, LONG_MAX, False, atom_pair, &type, &format, &len, &remaining, &data) == Success) {
if ((len >= 2) && data) {
Atom *targets = (Atom *)data;
for (uint64_t i = 0; i < len; i += 2) {
Atom target = targets[i];
Atom &property = targets[i + 1];
property = _process_selection_request_target(target, p_event->requestor, property);
}
XChangeProperty(x11_display,
p_event->requestor,
p_event->property,
atom_pair,
32,
PropModeReplace,
(unsigned char *)targets,
len);
respond.xselection.property = p_event->property;
}
XFree(data);
}
} else {
// Request for target conversion.
respond.xselection.property = _process_selection_request_target(p_event->target, p_event->requestor, p_event->property);
} }
respond.xselection.type = SelectionNotify; respond.xselection.type = SelectionNotify;
@ -2157,32 +2210,43 @@ Bool OS_X11::_predicate_all_events(Display *display, XEvent *event, XPointer arg
return True; return True;
} }
void OS_X11::_poll_events() { bool OS_X11::_wait_for_events() const {
int x11_fd = ConnectionNumber(x11_display); int x11_fd = ConnectionNumber(x11_display);
fd_set in_fds; fd_set in_fds;
while (!events_thread_done) { XFlush(x11_display);
XFlush(x11_display);
FD_ZERO(&in_fds); FD_ZERO(&in_fds);
FD_SET(x11_fd, &in_fds); FD_SET(x11_fd, &in_fds);
struct timeval tv; struct timeval tv;
tv.tv_usec = 0; tv.tv_usec = 0;
tv.tv_sec = 1; tv.tv_sec = 1;
// Wait for next event or timeout. // Wait for next event or timeout.
int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv); int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv);
if (num_ready_fds > 0) {
// Event received.
return true;
} else {
// Error or timeout.
if (num_ready_fds < 0) { if (num_ready_fds < 0) {
ERR_PRINT("_poll_events: select error: " + itos(errno)); ERR_PRINT("_wait_for_events: select error: " + itos(errno));
} }
return false;
}
}
void OS_X11::_poll_events() {
while (!events_thread_done) {
_wait_for_events();
// Process events from the queue. // Process events from the queue.
{ {
MutexLock mutex_lock(events_mutex); MutexLock mutex_lock(events_mutex);
// Non-blocking wait for next event // Non-blocking wait for next event and remove it from the queue.
// and remove it from the queue.
XEvent ev; XEvent ev;
while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, NULL)) { while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, NULL)) {
// Check if the input manager wants to process the event. // Check if the input manager wants to process the event.
@ -2787,6 +2851,10 @@ MainLoop *OS_X11::get_main_loop() const {
} }
void OS_X11::delete_main_loop() { void OS_X11::delete_main_loop() {
// Send owned clipboard data to clipboard manager before exit.
// This has to be done here because the clipboard data is cleared before finalize().
_clipboard_transfer_ownership(XA_PRIMARY, x11_window);
_clipboard_transfer_ownership(XInternAtom(x11_display, "CLIPBOARD", 0), x11_window);
if (main_loop) if (main_loop)
memdelete(main_loop); memdelete(main_loop);
@ -2823,60 +2891,137 @@ Bool OS_X11::_predicate_clipboard_selection(Display *display, XEvent *event, XPo
} }
} }
String OS_X11::_get_clipboard_impl(Atom p_source, Window x11_window, Atom target) const { Bool OS_X11::_predicate_clipboard_incr(Display *display, XEvent *event, XPointer arg) {
if (event->type == PropertyNotify && event->xproperty.state == PropertyNewValue) {
return True;
} else {
return False;
}
}
String OS_X11::_get_clipboard_impl(Atom p_source, Window x11_window, Atom target) const {
String ret; String ret;
Atom type;
Atom selection = XA_PRIMARY;
int format, result;
unsigned long len, bytes_left, dummy;
unsigned char *data;
Window selection_owner = XGetSelectionOwner(x11_display, p_source); Window selection_owner = XGetSelectionOwner(x11_display, p_source);
if (selection_owner == x11_window) { if (selection_owner == x11_window) {
return OS::get_clipboard(); return OS::get_clipboard();
} }
if (selection_owner != None) { if (selection_owner != None) {
{ // Block events polling while processing selection events.
// Block events polling while processing selection events. MutexLock mutex_lock(events_mutex);
MutexLock mutex_lock(events_mutex);
XConvertSelection(x11_display, p_source, target, selection, Atom selection = XA_PRIMARY;
x11_window, CurrentTime); XConvertSelection(x11_display, p_source, target, selection,
XFlush(x11_display); x11_window, CurrentTime);
// Blocking wait for predicate to be True XFlush(x11_display);
// and remove the event from the queue.
XEvent event;
XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window);
}
// // Blocking wait for predicate to be True and remove the event from the queue.
// Do not get any data, see how much data is there XEvent event;
// XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window);
// Do not get any data, see how much data is there.
Atom type;
int format, result;
unsigned long len, bytes_left, dummy;
unsigned char *data;
XGetWindowProperty(x11_display, x11_window, XGetWindowProperty(x11_display, x11_window,
selection, // Tricky.. selection, // Tricky..
0, 0, // offset - len 0, 0, // offset - len
0, // Delete 0==FALSE 0, // Delete 0==FALSE
AnyPropertyType, //flag AnyPropertyType, // flag
&type, // return type &type, // return type
&format, // return format &format, // return format
&len, &bytes_left, //that &len, &bytes_left, // data length
&data); &data);
// DATA is There
if (bytes_left > 0) { if (data) {
XFree(data);
}
if (type == XInternAtom(x11_display, "INCR", 0)) {
// Data is going to be received incrementally.
LocalVector<uint8_t> incr_data;
uint32_t data_size = 0;
bool success = false;
// Delete INCR property to notify the owner.
XDeleteProperty(x11_display, x11_window, type);
// Process events from the queue.
bool done = false;
while (!done) {
if (!_wait_for_events()) {
// Error or timeout, abort.
break;
}
// Non-blocking wait for next event and remove it from the queue.
XEvent ev;
while (XCheckIfEvent(x11_display, &ev, _predicate_clipboard_incr, NULL)) {
result = XGetWindowProperty(x11_display, x11_window,
selection, // selection type
0, LONG_MAX, // offset - len
True, // delete property to notify the owner
AnyPropertyType, // flag
&type, // return type
&format, // return format
&len, &bytes_left, // data length
&data);
if (result == Success) {
if (data && (len > 0)) {
uint32_t prev_size = incr_data.size();
if (prev_size == 0) {
// First property contains initial data size.
unsigned long initial_size = *(unsigned long *)data;
incr_data.resize(initial_size);
} else {
// New chunk, resize to be safe and append data.
incr_data.resize(MAX(data_size + len, prev_size));
memcpy(incr_data.ptr() + data_size, data, len);
data_size += len;
}
} else {
// Last chunk, process finished.
done = true;
success = true;
}
} else {
printf("Failed to get selection data chunk.\n");
done = true;
}
if (data) {
XFree(data);
}
if (done) {
break;
}
}
}
if (success && (data_size > 0)) {
ret.parse_utf8((const char *)incr_data.ptr(), data_size);
}
} else if (bytes_left > 0) {
// Data is ready and can be processed all at once.
result = XGetWindowProperty(x11_display, x11_window, result = XGetWindowProperty(x11_display, x11_window,
selection, 0, bytes_left, 0, selection, 0, bytes_left, 0,
AnyPropertyType, &type, &format, AnyPropertyType, &type, &format,
&len, &dummy, &data); &len, &dummy, &data);
if (result == Success) { if (result == Success) {
ret.parse_utf8((const char *)data); ret.parse_utf8((const char *)data);
} else } else {
printf("FAIL\n"); printf("Failed to get selection data.\n");
XFree(data); }
if (data) {
XFree(data);
}
} }
} }
@ -2907,6 +3052,58 @@ String OS_X11::get_clipboard() const {
return ret; return ret;
} }
Bool OS_X11::_predicate_clipboard_save_targets(Display *display, XEvent *event, XPointer arg) {
if (event->xany.window == *(Window *)arg) {
return (event->type == SelectionRequest) ||
(event->type == SelectionNotify);
} else {
return False;
}
}
void OS_X11::_clipboard_transfer_ownership(Atom p_source, Window x11_window) const {
Window selection_owner = XGetSelectionOwner(x11_display, p_source);
if (selection_owner != x11_window) {
return;
}
// Block events polling while processing selection events.
MutexLock mutex_lock(events_mutex);
Atom clipboard_manager = XInternAtom(x11_display, "CLIPBOARD_MANAGER", False);
Atom save_targets = XInternAtom(x11_display, "SAVE_TARGETS", False);
XConvertSelection(x11_display, clipboard_manager, save_targets, None,
x11_window, CurrentTime);
// Process events from the queue.
while (true) {
if (!_wait_for_events()) {
// Error or timeout, abort.
break;
}
// Non-blocking wait for next event and remove it from the queue.
XEvent ev;
while (XCheckIfEvent(x11_display, &ev, _predicate_clipboard_save_targets, (XPointer)&x11_window)) {
switch (ev.type) {
case SelectionRequest:
_handle_selection_request_event(&(ev.xselectionrequest));
break;
case SelectionNotify: {
if (ev.xselection.target == save_targets) {
// Once SelectionNotify is received, we're done whether it succeeded or not.
return;
}
break;
}
}
}
}
}
String OS_X11::get_name() const { String OS_X11::get_name() const {
return "X11"; return "X11";

View File

@ -157,20 +157,26 @@ class OS_X11 : public OS_Unix {
Point2i center; Point2i center;
void _handle_key_event(XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); void _handle_key_event(XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false);
void _handle_selection_request_event(XSelectionRequestEvent *p_event);
Atom _process_selection_request_target(Atom p_target, Window p_requestor, Atom p_property) const;
void _handle_selection_request_event(XSelectionRequestEvent *p_event) const;
String _get_clipboard_impl(Atom p_source, Window x11_window, Atom target) const; String _get_clipboard_impl(Atom p_source, Window x11_window, Atom target) const;
String _get_clipboard(Atom p_source, Window x11_window) const; String _get_clipboard(Atom p_source, Window x11_window) const;
void _clipboard_transfer_ownership(Atom p_source, Window x11_window) const;
mutable Mutex *events_mutex; mutable Mutex *events_mutex;
Thread *events_thread = nullptr; Thread *events_thread = nullptr;
bool events_thread_done = false; bool events_thread_done = false;
LocalVector<XEvent> polled_events; LocalVector<XEvent> polled_events;
static void _poll_events_thread(void *ud); static void _poll_events_thread(void *ud);
bool _wait_for_events() const;
void _poll_events(); void _poll_events();
static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg); static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg); static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_incr(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_save_targets(Display *display, XEvent *event, XPointer arg);
void process_xevents(); void process_xevents();
virtual void delete_main_loop(); virtual void delete_main_loop();