1// Clip Library
2// Copyright (c) 2018-2022 David Capello
3//
4// This file is released under the terms of the MIT license.
5// Read LICENSE.txt for more information.
6
7#include "clip.h"
8#include "clip_lock_impl.h"
9
10#include <xcb/xcb.h>
11
12#include <atomic>
13#include <algorithm>
14#include <cassert>
15#include <condition_variable>
16#include <cstdint>
17#include <cstdlib>
18#include <cstring>
19#include <functional>
20#include <map>
21#include <memory>
22#include <mutex>
23#include <thread>
24#include <vector>
25
26#ifdef HAVE_PNG_H
27 #include "clip_x11_png.h"
28#endif
29
30#define CLIP_SUPPORT_SAVE_TARGETS 1
31
32namespace clip {
33
34namespace {
35
36enum CommonAtom {
37 ATOM,
38 INCR,
39 TARGETS,
40 CLIPBOARD,
41#ifdef HAVE_PNG_H
42 MIME_IMAGE_PNG,
43#endif
44#ifdef CLIP_SUPPORT_SAVE_TARGETS
45 ATOM_PAIR,
46 SAVE_TARGETS,
47 MULTIPLE,
48 CLIPBOARD_MANAGER,
49#endif
50};
51
52const char* kCommonAtomNames[] = {
53 "ATOM",
54 "INCR",
55 "TARGETS",
56 "CLIPBOARD",
57#ifdef HAVE_PNG_H
58 "image/png",
59#endif
60#ifdef CLIP_SUPPORT_SAVE_TARGETS
61 "ATOM_PAIR",
62 "SAVE_TARGETS",
63 "MULTIPLE",
64 "CLIPBOARD_MANAGER",
65#endif
66};
67
68const int kBaseForCustomFormats = 100;
69
70class Manager {
71public:
72 typedef std::shared_ptr<std::vector<uint8_t>> buffer_ptr;
73 typedef std::vector<xcb_atom_t> atoms;
74 typedef std::function<bool()> notify_callback;
75
76 Manager()
77 : m_lock(m_mutex, std::defer_lock)
78 , m_connection(xcb_connect(nullptr, nullptr))
79 , m_window(0)
80 , m_incr_process(false) {
81 if (!m_connection)
82 return;
83
84 const xcb_setup_t* setup = xcb_get_setup(m_connection);
85 if (!setup)
86 return;
87
88 xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data;
89 if (!screen)
90 return;
91
92 uint32_t event_mask =
93 // Just in case that some program reports SelectionNotify events
94 // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
95 XCB_EVENT_MASK_PROPERTY_CHANGE |
96 // To receive DestroyNotify event and stop the message loop.
97 XCB_EVENT_MASK_STRUCTURE_NOTIFY;
98
99 m_window = xcb_generate_id(m_connection);
100 xcb_create_window(m_connection, 0,
101 m_window,
102 screen->root,
103 0, 0, 1, 1, 0,
104 XCB_WINDOW_CLASS_INPUT_OUTPUT,
105 screen->root_visual,
106 XCB_CW_EVENT_MASK,
107 &event_mask);
108
109 m_thread = std::thread(
110 [this]{
111 process_x11_events();
112 });
113 }
114
115 ~Manager() {
116#ifdef CLIP_SUPPORT_SAVE_TARGETS
117 if (!m_data.empty() &&
118 m_window &&
119 m_window == get_x11_selection_owner()) {
120 // If the CLIPBOARD_MANAGER atom is not 0, we assume that there
121 // is a clipboard manager available were we can leave our data.
122 xcb_atom_t x11_clipboard_manager = get_atom(CLIPBOARD_MANAGER);
123 if (x11_clipboard_manager) {
124 // We have to lock the m_lock mutex that will be used to wait
125 // the m_cv condition in get_data_from_selection_owner().
126 if (try_lock()) {
127 // Start the SAVE_TARGETS mechanism so the X11
128 // CLIPBOARD_MANAGER will save our clipboard data
129 // from now on.
130 get_data_from_selection_owner(
131 { get_atom(SAVE_TARGETS) },
132 []() -> bool { return true; },
133 x11_clipboard_manager);
134 }
135 }
136 }
137#endif
138
139 if (m_window) {
140 xcb_destroy_window(m_connection, m_window);
141 xcb_flush(m_connection);
142 }
143
144 if (m_thread.joinable())
145 m_thread.join();
146
147 if (m_connection)
148 xcb_disconnect(m_connection);
149 }
150
151 bool try_lock() {
152 bool res = m_lock.try_lock();
153 if (!res) {
154 // TODO make this configurable (the same for Windows retries)
155 for (int i=0; i<5 && !res; ++i) {
156 res = m_lock.try_lock();
157 std::this_thread::sleep_for(std::chrono::milliseconds(20));
158 }
159 }
160 return res;
161 }
162
163 void unlock() {
164 m_lock.unlock();
165 }
166
167 // Clear our data
168 void clear_data() {
169 m_data.clear();
170 m_image.reset();
171 }
172
173 void clear() {
174 clear_data();
175
176 // As we want to clear the clipboard content, we set us as the new
177 // clipboard owner (with an empty clipboard). If this fails, we'll
178 // try to send a XCB_SELECTION_CLEAR request to the real owner
179 // (but that can fail anyway because it's a request that the owner
180 // could ignore).
181 if (set_x11_selection_owner())
182 return;
183
184 // Clear the clipboard data from the selection owner
185 const xcb_window_t owner = get_x11_selection_owner();
186 if (m_window != owner) {
187 xcb_selection_clear_event_t event;
188 event.response_type = XCB_SELECTION_CLEAR;
189 event.pad0 = 0;
190 event.sequence = 0;
191 event.time = XCB_CURRENT_TIME;
192 event.owner = owner;
193 event.selection = get_atom(CLIPBOARD);
194
195 xcb_send_event(m_connection, false,
196 owner,
197 XCB_EVENT_MASK_NO_EVENT,
198 (const char*)&event);
199
200 xcb_flush(m_connection);
201 }
202 }
203
204 bool is_convertible(format f) const {
205 const atoms atoms = get_format_atoms(f);
206 const xcb_window_t owner = get_x11_selection_owner();
207
208 // If we are the owner, we just can check the m_data map
209 if (owner == m_window) {
210 for (xcb_atom_t atom : atoms) {
211 auto it = m_data.find(atom);
212 if (it != m_data.end())
213 return true;
214 }
215 }
216 // Ask to the selection owner the available formats/atoms/targets.
217 else if (owner) {
218 return
219 get_data_from_selection_owner(
220 { get_atom(TARGETS) },
221 [this, &atoms]() -> bool {
222 assert(m_reply_data);
223 if (!m_reply_data)
224 return false;
225
226 const xcb_atom_t* sel_atoms = (const xcb_atom_t*)&(*m_reply_data)[0];
227 int sel_natoms = m_reply_data->size() / sizeof(xcb_atom_t);
228 auto atoms_begin = atoms.begin();
229 auto atoms_end = atoms.end();
230 for (int i=0; i<sel_natoms; ++i) {
231 if (std::find(atoms_begin,
232 atoms_end,
233 sel_atoms[i]) != atoms_end) {
234 return true;
235 }
236 }
237 return false;
238 });
239 }
240
241 return false;
242 }
243
244 bool set_data(format f, const char* buf, size_t len) {
245 if (!set_x11_selection_owner())
246 return false;
247
248 const atoms atoms = get_format_atoms(f);
249 if (atoms.empty())
250 return false;
251
252 buffer_ptr shared_data_buf = std::make_shared<std::vector<uint8_t>>(len);
253 std::copy(buf,
254 buf+len,
255 shared_data_buf->begin());
256 for (xcb_atom_t atom : atoms)
257 m_data[atom] = shared_data_buf;
258
259 return true;
260 }
261
262 bool get_data(format f, char* buf, size_t len) const {
263 const atoms atoms = get_format_atoms(f);
264 const xcb_window_t owner = get_x11_selection_owner();
265 if (owner == m_window) {
266 for (xcb_atom_t atom : atoms) {
267 auto it = m_data.find(atom);
268 if (it != m_data.end()) {
269 size_t n = std::min(len, it->second->size());
270 std::copy(it->second->begin(),
271 it->second->begin()+n,
272 buf);
273
274 if (f == text_format()) {
275 // Add an extra null char
276 if (n < len)
277 buf[n] = 0;
278 }
279
280 return true;
281 }
282 }
283 }
284 else if (owner) {
285 if (get_data_from_selection_owner(
286 atoms,
287 [this, buf, len, f]() -> bool {
288 size_t n = std::min(len, m_reply_data->size());
289 std::copy(m_reply_data->begin(),
290 m_reply_data->begin()+n,
291 buf);
292
293 if (f == text_format()) {
294 if (n < len)
295 buf[n] = 0; // Include a null character
296 }
297
298 return true;
299 })) {
300 return true;
301 }
302 }
303 return false;
304 }
305
306 size_t get_data_length(format f) const {
307 size_t len = 0;
308 const atoms atoms = get_format_atoms(f);
309 const xcb_window_t owner = get_x11_selection_owner();
310 if (owner == m_window) {
311 for (xcb_atom_t atom : atoms) {
312 auto it = m_data.find(atom);
313 if (it != m_data.end()) {
314 len = it->second->size();
315 break;
316 }
317 }
318 }
319 else if (owner) {
320 if (!get_data_from_selection_owner(
321 atoms,
322 [this, &len]() -> bool {
323 len = m_reply_data->size();
324 return true;
325 })) {
326 // Error getting data length
327 return 0;
328 }
329 }
330 if (f == text_format() && len > 0) {
331 ++len; // Add an extra byte for the null char
332 }
333 return len;
334 }
335
336 bool set_image(const image& image) {
337 if (!set_x11_selection_owner())
338 return false;
339
340 m_image = image;
341
342#ifdef HAVE_PNG_H
343 // Put a nullptr in the m_data for image/png format and then we'll
344 // encode the png data when the image is requested in this format.
345 m_data[get_atom(MIME_IMAGE_PNG)] = buffer_ptr();
346#endif
347
348 return true;
349 }
350
351 bool get_image(image& output_img) const {
352 const xcb_window_t owner = get_x11_selection_owner();
353 if (owner == m_window) {
354 if (m_image.is_valid()) {
355 output_img = m_image;
356 return true;
357 }
358 }
359#ifdef HAVE_PNG_H
360 else if (owner &&
361 get_data_from_selection_owner(
362 { get_atom(MIME_IMAGE_PNG) },
363 [this, &output_img]() -> bool {
364 return x11::read_png(&(*m_reply_data)[0],
365 m_reply_data->size(),
366 &output_img, nullptr);
367 })) {
368 return true;
369 }
370#endif
371 return false;
372 }
373
374 bool get_image_spec(image_spec& spec) const {
375 const xcb_window_t owner = get_x11_selection_owner();
376 if (owner == m_window) {
377 if (m_image.is_valid()) {
378 spec = m_image.spec();
379 return true;
380 }
381 }
382#ifdef HAVE_PNG_H
383 else if (owner &&
384 get_data_from_selection_owner(
385 { get_atom(MIME_IMAGE_PNG) },
386 [this, &spec]() -> bool {
387 return x11::read_png(&(*m_reply_data)[0],
388 m_reply_data->size(),
389 nullptr, &spec);
390 })) {
391 return true;
392 }
393#endif
394 return false;
395 }
396
397 format register_format(const std::string& name) {
398 xcb_atom_t atom = get_atom(name.c_str());
399 m_custom_formats.push_back(atom);
400 return (format)(m_custom_formats.size()-1) + kBaseForCustomFormats;
401 }
402
403private:
404
405 void process_x11_events() {
406 bool stop = false;
407 xcb_generic_event_t* event;
408 while (!stop && (event = xcb_wait_for_event(m_connection))) {
409 int type = (event->response_type & ~0x80);
410
411 switch (type) {
412
413 case XCB_DESTROY_NOTIFY:
414 // To stop the message loop we can just destroy the window
415 stop = true;
416 break;
417
418 // Someone else has new content in the clipboard, so is
419 // notifying us that we should delete our data now.
420 case XCB_SELECTION_CLEAR:
421 handle_selection_clear_event(
422 (xcb_selection_clear_event_t*)event);
423 break;
424
425 // Someone is requesting the clipboard content from us.
426 case XCB_SELECTION_REQUEST:
427 handle_selection_request_event(
428 (xcb_selection_request_event_t*)event);
429 break;
430
431 // We've requested the clipboard content and this is the
432 // answer.
433 case XCB_SELECTION_NOTIFY:
434 handle_selection_notify_event(
435 (xcb_selection_notify_event_t*)event);
436 break;
437
438 case XCB_PROPERTY_NOTIFY:
439 handle_property_notify_event(
440 (xcb_property_notify_event_t*)event);
441 break;
442
443 }
444
445 free(event);
446 }
447 }
448
449 void handle_selection_clear_event(xcb_selection_clear_event_t* event) {
450 if (event->selection == get_atom(CLIPBOARD)) {
451 std::lock_guard<std::mutex> lock(m_mutex);
452 clear_data(); // Clear our clipboard data
453 }
454 }
455
456 void handle_selection_request_event(xcb_selection_request_event_t* event) {
457 std::lock_guard<std::mutex> lock(m_mutex);
458
459 if (event->target == get_atom(TARGETS)) {
460 atoms targets;
461 targets.push_back(get_atom(TARGETS));
462#ifdef CLIP_SUPPORT_SAVE_TARGETS
463 targets.push_back(get_atom(SAVE_TARGETS));
464 targets.push_back(get_atom(MULTIPLE));
465#endif
466 for (const auto& it : m_data)
467 targets.push_back(it.first);
468
469 // Set the "property" of "requestor" with the clipboard
470 // formats ("targets", atoms) that we provide.
471 xcb_change_property(
472 m_connection,
473 XCB_PROP_MODE_REPLACE,
474 event->requestor,
475 event->property,
476 get_atom(ATOM),
477 8*sizeof(xcb_atom_t),
478 targets.size(),
479 &targets[0]);
480 }
481#ifdef CLIP_SUPPORT_SAVE_TARGETS
482 else if (event->target == get_atom(SAVE_TARGETS)) {
483 // Do nothing
484 }
485 else if (event->target == get_atom(MULTIPLE)) {
486 xcb_get_property_reply_t* reply =
487 get_and_delete_property(event->requestor,
488 event->property,
489 get_atom(ATOM_PAIR),
490 false);
491 if (reply) {
492 for (xcb_atom_t
493 *ptr=(xcb_atom_t*)xcb_get_property_value(reply),
494 *end=ptr + (xcb_get_property_value_length(reply)/sizeof(xcb_atom_t));
495 ptr<end; ) {
496 xcb_atom_t target = *ptr++;
497 xcb_atom_t property = *ptr++;
498
499 if (!set_requestor_property_with_clipboard_content(
500 event->requestor,
501 property,
502 target)) {
503 xcb_change_property(
504 m_connection,
505 XCB_PROP_MODE_REPLACE,
506 event->requestor,
507 event->property,
508 XCB_ATOM_NONE, 0, 0, nullptr);
509 }
510 }
511
512 free(reply);
513 }
514 }
515#endif // CLIP_SUPPORT_SAVE_TARGETS
516 else {
517 if (!set_requestor_property_with_clipboard_content(
518 event->requestor,
519 event->property,
520 event->target)) {
521 return;
522 }
523 }
524
525 // Notify the "requestor" that we've already updated the property.
526 xcb_selection_notify_event_t notify;
527 notify.response_type = XCB_SELECTION_NOTIFY;
528 notify.pad0 = 0;
529 notify.sequence = 0;
530 notify.time = event->time;
531 notify.requestor = event->requestor;
532 notify.selection = event->selection;
533 notify.target = event->target;
534 notify.property = event->property;
535
536 xcb_send_event(m_connection, false,
537 event->requestor,
538 XCB_EVENT_MASK_NO_EVENT, // SelectionNotify events go without mask
539 (const char*)&notify);
540
541 xcb_flush(m_connection);
542 }
543
544 bool set_requestor_property_with_clipboard_content(const xcb_atom_t requestor,
545 const xcb_atom_t property,
546 const xcb_atom_t target) {
547 auto it = m_data.find(target);
548 if (it == m_data.end()) {
549 // Nothing to do (unsupported target)
550 return false;
551 }
552
553 // This can be null of the data was set from an image but we
554 // didn't encode the image yet (e.g. to image/png format).
555 if (!it->second) {
556 encode_data_on_demand(*it);
557
558 // Return nothing, the given "target" cannot be constructed
559 // (maybe by some encoding error).
560 if (!it->second)
561 return false;
562 }
563
564 // Set the "property" of "requestor" with the
565 // clipboard content in the requested format ("target").
566 xcb_change_property(
567 m_connection,
568 XCB_PROP_MODE_REPLACE,
569 requestor,
570 property,
571 target,
572 8,
573 it->second->size(),
574 &(*it->second)[0]);
575 return true;
576 }
577
578 void handle_selection_notify_event(xcb_selection_notify_event_t* event) {
579 assert(event->requestor == m_window);
580
581 if (event->target == get_atom(TARGETS))
582 m_target_atom = get_atom(ATOM);
583 else
584 m_target_atom = event->target;
585
586 xcb_get_property_reply_t* reply =
587 get_and_delete_property(event->requestor,
588 event->property,
589 m_target_atom);
590 if (reply) {
591 // In this case, We're going to receive the clipboard content in
592 // chunks of data with several PropertyNotify events.
593 if (reply->type == get_atom(INCR)) {
594 free(reply);
595
596 reply = get_and_delete_property(event->requestor,
597 event->property,
598 get_atom(INCR));
599 if (reply) {
600 if (xcb_get_property_value_length(reply) == 4) {
601 uint32_t n = *(uint32_t*)xcb_get_property_value(reply);
602 m_reply_data = std::make_shared<std::vector<uint8_t>>(n);
603 m_reply_offset = 0;
604 m_incr_process = true;
605 m_incr_received = true;
606 }
607 free(reply);
608 }
609 }
610 else {
611 // Simple case, the whole clipboard content in just one reply
612 // (without the INCR method).
613 m_reply_data.reset();
614 m_reply_offset = 0;
615 copy_reply_data(reply);
616
617 call_callback(reply);
618
619 free(reply);
620 }
621 }
622 }
623
624 void handle_property_notify_event(xcb_property_notify_event_t* event) {
625 if (m_incr_process &&
626 event->state == XCB_PROPERTY_NEW_VALUE &&
627 event->atom == get_atom(CLIPBOARD)) {
628 xcb_get_property_reply_t* reply =
629 get_and_delete_property(event->window,
630 event->atom,
631 m_target_atom);
632 if (reply) {
633 m_incr_received = true;
634
635 // When the length is 0 it means that the content was
636 // completely sent by the selection owner.
637 if (xcb_get_property_value_length(reply) > 0) {
638 copy_reply_data(reply);
639 }
640 else {
641 // Now that m_reply_data has the complete clipboard content,
642 // we can call the m_callback.
643 call_callback(reply);
644 m_incr_process = false;
645 }
646 free(reply);
647 }
648 }
649 }
650
651 xcb_get_property_reply_t* get_and_delete_property(xcb_window_t window,
652 xcb_atom_t property,
653 xcb_atom_t atom,
654 bool delete_prop = true) {
655 xcb_get_property_cookie_t cookie =
656 xcb_get_property(m_connection,
657 delete_prop,
658 window,
659 property,
660 atom,
661 0, 0x1fffffff); // 0x1fffffff = INT32_MAX / 4
662
663 xcb_generic_error_t* err = nullptr;
664 xcb_get_property_reply_t* reply =
665 xcb_get_property_reply(m_connection, cookie, &err);
666 if (err) {
667 // TODO report error
668 free(err);
669 }
670 return reply;
671 }
672
673 // Concatenates the new data received in "reply" into "m_reply_data"
674 // buffer.
675 void copy_reply_data(xcb_get_property_reply_t* reply) {
676 const uint8_t* src = (const uint8_t*)xcb_get_property_value(reply);
677 // n = length of "src" in bytes
678 size_t n = xcb_get_property_value_length(reply);
679
680 size_t req = m_reply_offset+n;
681 if (!m_reply_data) {
682 m_reply_data = std::make_shared<std::vector<uint8_t>>(req);
683 }
684 // The "m_reply_data" size can be smaller because the size
685 // specified in INCR property is just a lower bound.
686 else if (req > m_reply_data->size()) {
687 m_reply_data->resize(req);
688 }
689
690 std::copy(src, src+n, m_reply_data->begin()+m_reply_offset);
691 m_reply_offset += n;
692 }
693
694 // Calls the current m_callback() to handle the clipboard content
695 // received from the owner.
696 void call_callback(xcb_get_property_reply_t* reply) {
697 m_callback_result = false;
698 if (m_callback)
699 m_callback_result = m_callback();
700
701 m_cv.notify_one();
702
703 m_reply_data.reset();
704 }
705
706 bool get_data_from_selection_owner(const atoms& atoms,
707 const notify_callback&& callback,
708 xcb_atom_t selection = 0) const {
709 if (!selection)
710 selection = get_atom(CLIPBOARD);
711
712 // Put the callback on "m_callback" so we can call it on
713 // SelectionNotify event.
714 m_callback = std::move(callback);
715
716 // Clear data if we are not the selection owner.
717 if (m_window != get_x11_selection_owner())
718 m_data.clear();
719
720 // Ask to the selection owner for its content on each known
721 // text format/atom.
722 for (xcb_atom_t atom : atoms) {
723 xcb_convert_selection(m_connection,
724 m_window, // Send us the result
725 selection, // Clipboard selection
726 atom, // The clipboard format that we're requesting
727 get_atom(CLIPBOARD), // Leave result in this window's property
728 XCB_CURRENT_TIME);
729
730 xcb_flush(m_connection);
731
732 // We use the "m_incr_received" to wait several timeouts in case
733 // that we've received the INCR SelectionNotify or
734 // PropertyNotify events.
735 do {
736 m_incr_received = false;
737
738 // Wait a response for 100 milliseconds
739 std::cv_status status =
740 m_cv.wait_for(m_lock,
741 std::chrono::milliseconds(get_x11_wait_timeout()));
742 if (status == std::cv_status::no_timeout) {
743 // If the condition variable was notified, it means that the
744 // callback was called correctly.
745 return m_callback_result;
746 }
747 } while (m_incr_received);
748 }
749
750 // Reset callback
751 m_callback = notify_callback();
752 return false;
753 }
754
755 atoms get_atoms(const char** names,
756 const int n) const {
757 atoms result(n, 0);
758 std::vector<xcb_intern_atom_cookie_t> cookies(n);
759
760 for (int i=0; i<n; ++i) {
761 auto it = m_atoms.find(names[i]);
762 if (it != m_atoms.end())
763 result[i] = it->second;
764 else
765 cookies[i] = xcb_intern_atom(
766 m_connection, 0,
767 std::strlen(names[i]), names[i]);
768 }
769
770 for (int i=0; i<n; ++i) {
771 if (result[i] == 0) {
772 xcb_intern_atom_reply_t* reply =
773 xcb_intern_atom_reply(m_connection,
774 cookies[i],
775 nullptr);
776 if (reply) {
777 result[i] = m_atoms[names[i]] = reply->atom;
778 free(reply);
779 }
780 }
781 }
782
783 return result;
784 }
785
786 xcb_atom_t get_atom(const char* name) const {
787 auto it = m_atoms.find(name);
788 if (it != m_atoms.end())
789 return it->second;
790
791 xcb_atom_t result = 0;
792 xcb_intern_atom_cookie_t cookie =
793 xcb_intern_atom(m_connection, 0,
794 std::strlen(name), name);
795
796 xcb_intern_atom_reply_t* reply =
797 xcb_intern_atom_reply(m_connection,
798 cookie,
799 nullptr);
800 if (reply) {
801 result = m_atoms[name] = reply->atom;
802 free(reply);
803 }
804 return result;
805 }
806
807 xcb_atom_t get_atom(CommonAtom i) const {
808 if (m_common_atoms.empty()) {
809 m_common_atoms =
810 get_atoms(kCommonAtomNames,
811 sizeof(kCommonAtomNames) / sizeof(kCommonAtomNames[0]));
812 }
813 return m_common_atoms[i];
814 }
815
816 const atoms& get_text_format_atoms() const {
817 if (m_text_atoms.empty()) {
818 const char* names[] = {
819 // Prefer utf-8 formats first
820 "UTF8_STRING",
821 "text/plain;charset=utf-8",
822 "text/plain;charset=UTF-8",
823 "GTK_TEXT_BUFFER_CONTENTS", // Required for gedit (and maybe gtk+ apps)
824 // ANSI C strings?
825 "STRING",
826 "TEXT",
827 "text/plain",
828 };
829 m_text_atoms = get_atoms(names, sizeof(names) / sizeof(names[0]));
830 }
831 return m_text_atoms;
832 }
833
834 const atoms& get_image_format_atoms() const {
835 if (m_image_atoms.empty()) {
836#ifdef HAVE_PNG_H
837 m_image_atoms.push_back(get_atom(MIME_IMAGE_PNG));
838#endif
839 }
840 return m_image_atoms;
841 }
842
843 atoms get_format_atoms(const format f) const {
844 atoms atoms;
845 if (f == text_format()) {
846 atoms = get_text_format_atoms();
847 }
848 else if (f == image_format()) {
849 atoms = get_image_format_atoms();
850 }
851 else {
852 xcb_atom_t atom = get_format_atom(f);
853 if (atom)
854 atoms.push_back(atom);
855 }
856 return atoms;
857 }
858
859#if !defined(NDEBUG)
860 // This can be used to print debugging messages.
861 std::string get_atom_name(xcb_atom_t atom) const {
862 std::string result;
863 xcb_get_atom_name_cookie_t cookie =
864 xcb_get_atom_name(m_connection, atom);
865 xcb_generic_error_t* err = nullptr;
866 xcb_get_atom_name_reply_t* reply =
867 xcb_get_atom_name_reply(m_connection, cookie, &err);
868 if (err) {
869 free(err);
870 }
871 if (reply) {
872 int len = xcb_get_atom_name_name_length(reply);
873 if (len > 0) {
874 result.resize(len);
875 char* name = xcb_get_atom_name_name(reply);
876 if (name)
877 std::copy(name, name+len, result.begin());
878 }
879 free(reply);
880 }
881 return result;
882 }
883#endif
884
885 bool set_x11_selection_owner() const {
886 xcb_void_cookie_t cookie =
887 xcb_set_selection_owner_checked(m_connection,
888 m_window,
889 get_atom(CLIPBOARD),
890 XCB_CURRENT_TIME);
891 xcb_generic_error_t* err =
892 xcb_request_check(m_connection,
893 cookie);
894 if (err) {
895 free(err);
896 return false;
897 }
898 return true;
899 }
900
901 xcb_window_t get_x11_selection_owner() const {
902 xcb_window_t result = 0;
903 xcb_get_selection_owner_cookie_t cookie =
904 xcb_get_selection_owner(m_connection,
905 get_atom(CLIPBOARD));
906
907 xcb_get_selection_owner_reply_t* reply =
908 xcb_get_selection_owner_reply(m_connection, cookie, nullptr);
909 if (reply) {
910 result = reply->owner;
911 free(reply);
912 }
913 return result;
914 }
915
916 xcb_atom_t get_format_atom(const format f) const {
917 int i = f - kBaseForCustomFormats;
918 if (i >= 0 && i < int(m_custom_formats.size()))
919 return m_custom_formats[i];
920 else
921 return 0;
922 }
923
924 void encode_data_on_demand(std::pair<const xcb_atom_t, buffer_ptr>& e) {
925#ifdef HAVE_PNG_H
926 if (e.first == get_atom(MIME_IMAGE_PNG)) {
927 assert(m_image.is_valid());
928 if (!m_image.is_valid())
929 return;
930
931 std::vector<uint8_t> output;
932 if (x11::write_png(m_image, output)) {
933 e.second =
934 std::make_shared<std::vector<uint8_t>>(
935 std::move(output));
936 }
937 // else { TODO report png conversion errors }
938 }
939#endif
940 }
941
942 // Access to the whole Manager
943 std::mutex m_mutex;
944
945 // Lock used in the main thread using the Manager (i.e. by lock::impl)
946 mutable std::unique_lock<std::mutex> m_lock;
947
948 // Connection to X11 server
949 xcb_connection_t* m_connection;
950
951 // Temporal background window used to own the clipboard and process
952 // all events related about the clipboard in a background thread
953 xcb_window_t m_window;
954
955 // Used to wait/notify the arrival of the SelectionNotify event when
956 // we requested the clipboard content from other selection owner.
957 mutable std::condition_variable m_cv;
958
959 // Thread used to run a background message loop to wait X11 events
960 // about clipboard. The X11 selection owner will be a hidden window
961 // created by us just for the clipboard purpose/communication.
962 std::thread m_thread;
963
964 // Internal callback used when a SelectionNotify is received (or the
965 // whole data content is received by the INCR method). So this
966 // callback can use the notification by different purposes (e.g. get
967 // the data length only, or get/process the data content, etc.).
968 mutable notify_callback m_callback;
969
970 // Result returned by the m_callback. Used as return value in the
971 // get_data_from_selection_owner() function. For example, if the
972 // callback must read a "image/png" file from the clipboard data and
973 // fails, the callback can return false and finally the get_image()
974 // will return false (i.e. there is data, but it's not a valid image
975 // format).
976 std::atomic<bool> m_callback_result;
977
978 // Cache of known atoms
979 mutable std::map<std::string, xcb_atom_t> m_atoms;
980
981 // Cache of common used atoms by us
982 mutable atoms m_common_atoms;
983
984 // Cache of atoms related to text or image content
985 mutable atoms m_text_atoms;
986 mutable atoms m_image_atoms;
987
988 // Actual clipboard data generated by us (when we "copy" content in
989 // the clipboard, it means that we own the X11 "CLIPBOARD"
990 // selection, and in case of SelectionRequest events, we've to
991 // return the data stored in this "m_data" field)
992 mutable std::map<xcb_atom_t, buffer_ptr> m_data;
993
994 // Copied image in the clipboard. As we have to transfer the image
995 // in some specific format (e.g. image/png) we want to keep a copy
996 // of the image and make the conversion when the clipboard data is
997 // requested by other process.
998 mutable image m_image;
999
1000 // True if we have received an INCR notification so we're going to
1001 // process several PropertyNotify to concatenate all data chunks.
1002 bool m_incr_process;
1003
1004 // Variable used to wait more time if we've received an INCR
1005 // notification, which means that we're going to receive large
1006 // amounts of data from the selection owner.
1007 mutable bool m_incr_received;
1008
1009 // Target/selection format used in the SelectionNotify. Used in the
1010 // INCR method to get data from the same property in the same format
1011 // (target) on each PropertyNotify.
1012 xcb_atom_t m_target_atom;
1013
1014 // Each time we receive data from the selection owner, we put that
1015 // data in this buffer. If we get the data with the INCR method,
1016 // we'll concatenate chunks of data in this buffer to complete the
1017 // whole clipboard content.
1018 buffer_ptr m_reply_data;
1019
1020 // Used to concatenate chunks of data in "m_reply_data" from several
1021 // PropertyNotify when we are getting the selection owner data with
1022 // the INCR method.
1023 size_t m_reply_offset;
1024
1025 // List of user-defined formats/atoms.
1026 std::vector<xcb_atom_t> m_custom_formats;
1027};
1028
1029Manager* manager = nullptr;
1030
1031void delete_manager_atexit() {
1032 if (manager) {
1033 delete manager;
1034 manager = nullptr;
1035 }
1036}
1037
1038Manager* get_manager() {
1039 if (!manager) {
1040 manager = new Manager;
1041 std::atexit(delete_manager_atexit);
1042 }
1043 return manager;
1044}
1045
1046} // anonymous namespace
1047
1048lock::impl::impl(void*) : m_locked(false) {
1049 m_locked = get_manager()->try_lock();
1050}
1051
1052lock::impl::~impl() {
1053 if (m_locked)
1054 manager->unlock();
1055}
1056
1057bool lock::impl::clear() {
1058 manager->clear();
1059 return true;
1060}
1061
1062bool lock::impl::is_convertible(format f) const {
1063 return manager->is_convertible(f);
1064}
1065
1066bool lock::impl::set_data(format f, const char* buf, size_t len) {
1067 return manager->set_data(f, buf, len);
1068}
1069
1070bool lock::impl::get_data(format f, char* buf, size_t len) const {
1071 return manager->get_data(f, buf, len);
1072}
1073
1074size_t lock::impl::get_data_length(format f) const {
1075 return manager->get_data_length(f);
1076}
1077
1078bool lock::impl::set_image(const image& image) {
1079 return manager->set_image(image);
1080}
1081
1082bool lock::impl::get_image(image& output_img) const {
1083 return manager->get_image(output_img);
1084}
1085
1086bool lock::impl::get_image_spec(image_spec& spec) const {
1087 return manager->get_image_spec(spec);
1088}
1089
1090format register_format(const std::string& name) {
1091 return get_manager()->register_format(name);
1092}
1093
1094} // namespace clip
1095