Parcourir la source

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.
PouleyKetchoupp il y a 5 ans
Parent
commit
de4a1d09b0
2 fichiers modifiés avec 282 ajouts et 79 suppressions
  1. 275 78
      platform/x11/os_x11.cpp
  2. 7 1
      platform/x11/os_x11.h

+ 275 - 78
platform/x11/os_x11.cpp

@@ -813,7 +813,7 @@ void OS_X11::finalize() {
 	events_thread_done = true;
 	Thread::wait_to_finish(events_thread);
 	memdelete(events_thread);
-	events_thread = nullptr;
+	events_thread = NULL;
 
 	if (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);
 }
 
-void OS_X11::_handle_selection_request_event(XSelectionRequestEvent *p_event) {
-	XEvent respond;
-	if (p_event->target == XInternAtom(x11_display, "UTF8_STRING", 0) ||
-			p_event->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) ||
-			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];
+Atom OS_X11::_process_selection_request_target(Atom p_target, Window p_requestor, Atom p_property) const {
+	if (p_target == XInternAtom(x11_display, "TARGETS", 0)) {
+		// Request to list all supported targets.
+		Atom data[9];
 		data[0] = XInternAtom(x11_display, "TARGETS", 0);
-		data[1] = XInternAtom(x11_display, "UTF8_STRING", 0);
-		data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0);
-		data[3] = XInternAtom(x11_display, "TEXT", 0);
-		data[4] = XA_STRING;
-		data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0);
-		data[6] = XInternAtom(x11_display, "text/plain", 0);
+		data[1] = XInternAtom(x11_display, "SAVE_TARGETS", 0);
+		data[2] = XInternAtom(x11_display, "MULTIPLE", 0);
+		data[3] = XInternAtom(x11_display, "UTF8_STRING", 0);
+		data[4] = XInternAtom(x11_display, "COMPOUND_TEXT", 0);
+		data[5] = XInternAtom(x11_display, "TEXT", 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,
-				p_event->requestor,
-				p_event->property,
+				p_requestor,
+				p_property,
 				XA_ATOM,
 				32,
 				PropModeReplace,
 				(unsigned char *)&data,
 				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 {
-		char *targetname = XGetAtomName(x11_display, p_event->target);
-		printf("No Target '%s'\n", targetname);
-		if (targetname) {
-			XFree(targetname);
+		char *target_name = XGetAtomName(x11_display, p_target);
+		printf("Target '%s' not supported.\n", target_name);
+		if (target_name) {
+			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;
+
+		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;
@@ -2157,32 +2210,43 @@ Bool OS_X11::_predicate_all_events(Display *display, XEvent *event, XPointer arg
 	return True;
 }
 
-void OS_X11::_poll_events() {
+bool OS_X11::_wait_for_events() const {
 	int x11_fd = ConnectionNumber(x11_display);
 	fd_set in_fds;
 
-	while (!events_thread_done) {
-		XFlush(x11_display);
+	XFlush(x11_display);
+
+	FD_ZERO(&in_fds);
+	FD_SET(x11_fd, &in_fds);
 
-		FD_ZERO(&in_fds);
-		FD_SET(x11_fd, &in_fds);
+	struct timeval tv;
+	tv.tv_usec = 0;
+	tv.tv_sec = 1;
 
-		struct timeval tv;
-		tv.tv_usec = 0;
-		tv.tv_sec = 1;
+	// Wait for next event or timeout.
+	int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv);
 
-		// Wait for next event or timeout.
-		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) {
-			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.
 		{
 			MutexLock mutex_lock(events_mutex);
 
-			// Non-blocking wait for next event
-			// and remove it from the queue.
+			// Non-blocking wait for next event and remove it from the queue.
 			XEvent ev;
 			while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, NULL)) {
 				// 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() {
+	// 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)
 		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;
 
-	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);
-
 	if (selection_owner == x11_window) {
-
 		return OS::get_clipboard();
 	}
 
 	if (selection_owner != None) {
-		{
-			// Block events polling while processing selection events.
-			MutexLock mutex_lock(events_mutex);
+		// Block events polling while processing selection events.
+		MutexLock mutex_lock(events_mutex);
 
-			XConvertSelection(x11_display, p_source, target, selection,
-					x11_window, CurrentTime);
-			XFlush(x11_display);
+		Atom selection = XA_PRIMARY;
+		XConvertSelection(x11_display, p_source, target, selection,
+				x11_window, CurrentTime);
 
-			// Blocking wait for predicate to be True
-			// and remove the event from the queue.
-			XEvent event;
-			XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window);
-		}
+		XFlush(x11_display);
 
-		//
-		// Do not get any data, see how much data is there
-		//
+		// Blocking wait for predicate to be True and remove the event from the queue.
+		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,
 				selection, // Tricky..
 				0, 0, // offset - len
 				0, // Delete 0==FALSE
-				AnyPropertyType, //flag
+				AnyPropertyType, // flag
 				&type, // return type
 				&format, // return format
-				&len, &bytes_left, //that
+				&len, &bytes_left, // data length
 				&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,
 					selection, 0, bytes_left, 0,
 					AnyPropertyType, &type, &format,
 					&len, &dummy, &data);
+
 			if (result == Success) {
 				ret.parse_utf8((const char *)data);
-			} else
-				printf("FAIL\n");
-			XFree(data);
+			} else {
+				printf("Failed to get selection data.\n");
+			}
+
+			if (data) {
+				XFree(data);
+			}
 		}
 	}
 
@@ -2907,6 +3052,58 @@ String OS_X11::get_clipboard() const {
 	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 {
 
 	return "X11";

+ 7 - 1
platform/x11/os_x11.h

@@ -157,20 +157,26 @@ class OS_X11 : public OS_Unix {
 	Point2i center;
 
 	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(Atom p_source, Window x11_window) const;
+	void _clipboard_transfer_ownership(Atom p_source, Window x11_window) const;
 
 	mutable Mutex *events_mutex;
 	Thread *events_thread = nullptr;
 	bool events_thread_done = false;
 	LocalVector<XEvent> polled_events;
 	static void _poll_events_thread(void *ud);
+	bool _wait_for_events() const;
 	void _poll_events();
 
 	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_incr(Display *display, XEvent *event, XPointer arg);
+	static Bool _predicate_clipboard_save_targets(Display *display, XEvent *event, XPointer arg);
 
 	void process_xevents();
 	virtual void delete_main_loop();