1 | /* |
2 | SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org> |
3 | SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "waylandclipboard_p.h" |
9 | |
10 | #include <QBuffer> |
11 | #include <QFile> |
12 | #include <QGuiApplication> |
13 | #include <QImageReader> |
14 | #include <QImageWriter> |
15 | #include <QMimeData> |
16 | #include <QPointer> |
17 | #include <QWaylandClientExtension> |
18 | #include <QWindow> |
19 | #include <QtWaylandClientVersion> |
20 | |
21 | #include <errno.h> |
22 | #include <fcntl.h> |
23 | #include <poll.h> |
24 | #include <signal.h> |
25 | #include <string.h> |
26 | #include <unistd.h> |
27 | |
28 | #include "qwayland-wayland.h" |
29 | #include "qwayland-ext-data-control-v1.h" |
30 | |
31 | static inline QString applicationQtXImageLiteral() |
32 | { |
33 | return QStringLiteral("application/x-qt-image" ); |
34 | } |
35 | |
36 | // copied from https://code.woboq.org/qt5/qtbase/src/gui/kernel/qinternalmimedata.cpp.html |
37 | static QString utf8Text() |
38 | { |
39 | return QStringLiteral("text/plain;charset=utf-8" ); |
40 | } |
41 | |
42 | static QStringList imageMimeFormats(const QList<QByteArray> &imageFormats) |
43 | { |
44 | QStringList formats; |
45 | formats.reserve(asize: imageFormats.size()); |
46 | for (const auto &format : imageFormats) |
47 | formats.append(t: QLatin1String("image/" ) + QLatin1String(format.toLower())); |
48 | // put png at the front because it is best |
49 | int pngIndex = formats.indexOf(needle: QLatin1String("image/png" )); |
50 | if (pngIndex != -1 && pngIndex != 0) |
51 | formats.move(from: pngIndex, to: 0); |
52 | return formats; |
53 | } |
54 | |
55 | static inline QStringList imageReadMimeFormats() |
56 | { |
57 | return imageMimeFormats(imageFormats: QImageReader::supportedImageFormats()); |
58 | } |
59 | |
60 | static inline QStringList imageWriteMimeFormats() |
61 | { |
62 | return imageMimeFormats(imageFormats: QImageWriter::supportedImageFormats()); |
63 | } |
64 | // end copied |
65 | |
66 | class DataControlDeviceManager : public QWaylandClientExtensionTemplate<DataControlDeviceManager>, public QtWayland::ext_data_control_manager_v1 |
67 | { |
68 | Q_OBJECT |
69 | public: |
70 | DataControlDeviceManager() |
71 | : QWaylandClientExtensionTemplate<DataControlDeviceManager>(1) |
72 | { |
73 | } |
74 | |
75 | void instantiate() |
76 | { |
77 | initialize(); |
78 | } |
79 | |
80 | ~DataControlDeviceManager() |
81 | { |
82 | if (isInitialized()) { |
83 | destroy(); |
84 | } |
85 | } |
86 | }; |
87 | |
88 | class DataControlOffer : public QMimeData, public QtWayland::ext_data_control_offer_v1 |
89 | { |
90 | Q_OBJECT |
91 | public: |
92 | DataControlOffer(struct ::ext_data_control_offer_v1 *id) |
93 | : QtWayland::ext_data_control_offer_v1(id) |
94 | { |
95 | } |
96 | |
97 | ~DataControlOffer() |
98 | { |
99 | destroy(); |
100 | } |
101 | |
102 | QStringList formats() const override |
103 | { |
104 | return m_receivedFormats; |
105 | } |
106 | |
107 | bool containsImageData() const |
108 | { |
109 | if (m_receivedFormats.contains(str: applicationQtXImageLiteral())) { |
110 | return true; |
111 | } |
112 | const auto formats = imageReadMimeFormats(); |
113 | for (const auto &receivedFormat : m_receivedFormats) { |
114 | if (formats.contains(str: receivedFormat)) { |
115 | return true; |
116 | } |
117 | } |
118 | return false; |
119 | } |
120 | |
121 | bool hasFormat(const QString &mimeType) const override |
122 | { |
123 | if (mimeType == QStringLiteral("text/plain" ) && m_receivedFormats.contains(str: utf8Text())) { |
124 | return true; |
125 | } |
126 | if (m_receivedFormats.contains(str: mimeType)) { |
127 | return true; |
128 | } |
129 | |
130 | // If we have image data |
131 | if (containsImageData()) { |
132 | // is the requested output mimeType supported ? |
133 | const QStringList imageFormats = imageWriteMimeFormats(); |
134 | for (const QString &imageFormat : imageFormats) { |
135 | if (imageFormat == mimeType) { |
136 | return true; |
137 | } |
138 | } |
139 | if (mimeType == applicationQtXImageLiteral()) { |
140 | return true; |
141 | } |
142 | } |
143 | |
144 | return false; |
145 | } |
146 | |
147 | protected: |
148 | void ext_data_control_offer_v1_offer(const QString &mime_type) override |
149 | { |
150 | if (!m_receivedFormats.contains(str: mime_type)) { |
151 | m_receivedFormats << mime_type; |
152 | } |
153 | } |
154 | |
155 | QVariant retrieveData(const QString &mimeType, QMetaType type) const override; |
156 | |
157 | private: |
158 | /* reads data from a file descriptor with a timeout of 1 second |
159 | * true if data is read successfully |
160 | */ |
161 | static bool readData(int fd, QByteArray &data, const QString &mimeType); |
162 | QStringList m_receivedFormats; |
163 | mutable QHash<QString, QVariant> m_data; |
164 | }; |
165 | |
166 | QVariant DataControlOffer::retrieveData(const QString &mimeType, QMetaType type) const |
167 | { |
168 | Q_UNUSED(type); |
169 | |
170 | auto it = m_data.constFind(key: mimeType); |
171 | if (it != m_data.constEnd()) |
172 | return *it; |
173 | |
174 | QString mime; |
175 | if (!m_receivedFormats.contains(str: mimeType)) { |
176 | if (mimeType == QStringLiteral("text/plain" ) && m_receivedFormats.contains(str: utf8Text())) { |
177 | mime = utf8Text(); |
178 | } else if (mimeType == applicationQtXImageLiteral()) { |
179 | const auto writeFormats = imageWriteMimeFormats(); |
180 | for (const auto &receivedFormat : m_receivedFormats) { |
181 | if (writeFormats.contains(str: receivedFormat)) { |
182 | mime = receivedFormat; |
183 | break; |
184 | } |
185 | } |
186 | if (mime.isEmpty()) { |
187 | // default exchange format |
188 | mime = QStringLiteral("image/png" ); |
189 | } |
190 | } |
191 | |
192 | if (mime.isEmpty()) { |
193 | return QVariant(); |
194 | } |
195 | } else { |
196 | mime = mimeType; |
197 | } |
198 | |
199 | int pipeFds[2]; |
200 | if (pipe(pipedes: pipeFds) != 0) { |
201 | return QVariant(); |
202 | } |
203 | |
204 | auto t = const_cast<DataControlOffer *>(this); |
205 | t->receive(mime_type: mime, fd: pipeFds[1]); |
206 | |
207 | close(fd: pipeFds[1]); |
208 | |
209 | /* |
210 | * Ideally we need to introduce a non-blocking QMimeData object |
211 | * Or a non-blocking constructor to QMimeData with the mimetypes that are relevant |
212 | * |
213 | * However this isn't actually any worse than X. |
214 | */ |
215 | |
216 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
217 | auto display = waylandApp->display(); |
218 | |
219 | wl_display_flush(display); |
220 | |
221 | QFile readPipe; |
222 | if (readPipe.open(fd: pipeFds[0], ioFlags: QIODevice::ReadOnly)) { |
223 | QByteArray data; |
224 | if (readData(fd: pipeFds[0], data, mimeType: mime)) { |
225 | close(fd: pipeFds[0]); |
226 | |
227 | if (mimeType == applicationQtXImageLiteral()) { |
228 | QImage img = QImage::fromData(data, format: mime.mid(position: mime.indexOf(ch: QLatin1Char('/')) + 1).toLatin1().toUpper().data()); |
229 | if (!img.isNull()) { |
230 | m_data.insert(key: mimeType, value: img); |
231 | return img; |
232 | } |
233 | } else if (data.size() > 1 && mimeType == u"text/uri-list" ) { |
234 | const auto urls = data.split(sep: '\n'); |
235 | QVariantList list; |
236 | list.reserve(asize: urls.size()); |
237 | for (const QByteArray &s : urls) { |
238 | if (QUrl url(QUrl::fromEncoded(input: QByteArrayView(s).trimmed())); url.isValid()) { |
239 | list.emplace_back(args: std::move(url)); |
240 | } |
241 | } |
242 | m_data.insert(key: mimeType, value: list); |
243 | return list; |
244 | } |
245 | m_data.insert(key: mimeType, value: data); |
246 | return data; |
247 | } |
248 | close(fd: pipeFds[0]); |
249 | } |
250 | |
251 | return QVariant(); |
252 | } |
253 | |
254 | bool DataControlOffer::readData(int fd, QByteArray &data, const QString &mimeType) |
255 | { |
256 | pollfd pfds[1]; |
257 | pfds[0].fd = fd; |
258 | pfds[0].events = POLLIN; |
259 | |
260 | while (true) { |
261 | const int ready = poll(fds: pfds, nfds: 1, timeout: 1000); |
262 | if (ready < 0) { |
263 | if (errno != EINTR) { |
264 | qWarning(msg: "DataControlOffer: poll() failed for mimeType %s: %s" , qPrintable(mimeType), strerror(errno)); |
265 | return false; |
266 | } |
267 | } else if (ready == 0) { |
268 | qWarning(msg: "DataControlOffer: timeout reading from pipe for mimeType %s" , qPrintable(mimeType)); |
269 | return false; |
270 | } else { |
271 | char buf[4096]; |
272 | int n = read(fd: fd, buf: buf, nbytes: sizeof buf); |
273 | |
274 | if (n < 0) { |
275 | qWarning(msg: "DataControlOffer: read() failed for mimeType %s: %s" , qPrintable(mimeType), strerror(errno)); |
276 | return false; |
277 | } else if (n == 0) { |
278 | return true; |
279 | } else if (n > 0) { |
280 | data.append(s: buf, len: n); |
281 | } |
282 | } |
283 | } |
284 | } |
285 | |
286 | class DataControlSource : public QObject, public QtWayland::ext_data_control_source_v1 |
287 | { |
288 | Q_OBJECT |
289 | public: |
290 | DataControlSource(struct ::ext_data_control_source_v1 *id, QMimeData *mimeData); |
291 | DataControlSource() = default; |
292 | ~DataControlSource() |
293 | { |
294 | destroy(); |
295 | } |
296 | |
297 | QMimeData *mimeData() |
298 | { |
299 | return m_mimeData.get(); |
300 | } |
301 | std::unique_ptr<QMimeData> releaseMimeData() |
302 | { |
303 | return std::move(m_mimeData); |
304 | } |
305 | |
306 | Q_SIGNALS: |
307 | void cancelled(); |
308 | |
309 | protected: |
310 | void ext_data_control_source_v1_send(const QString &mime_type, int32_t fd) override; |
311 | void ext_data_control_source_v1_cancelled() override; |
312 | |
313 | private: |
314 | std::unique_ptr<QMimeData> m_mimeData; |
315 | }; |
316 | |
317 | DataControlSource::DataControlSource(struct ::ext_data_control_source_v1 *id, QMimeData *mimeData) |
318 | : QtWayland::ext_data_control_source_v1(id) |
319 | , m_mimeData(mimeData) |
320 | { |
321 | const auto formats = mimeData->formats(); |
322 | for (const QString &format : formats) { |
323 | offer(mime_type: format); |
324 | } |
325 | if (mimeData->hasText()) { |
326 | // ensure GTK applications get this mimetype to avoid them discarding the offer |
327 | offer(QStringLiteral("text/plain;charset=utf-8" )); |
328 | } |
329 | |
330 | if (mimeData->hasImage()) { |
331 | const QStringList imageFormats = imageWriteMimeFormats(); |
332 | for (const QString &imageFormat : imageFormats) { |
333 | if (!formats.contains(str: imageFormat)) { |
334 | offer(mime_type: imageFormat); |
335 | } |
336 | } |
337 | } |
338 | } |
339 | |
340 | void DataControlSource::ext_data_control_source_v1_send(const QString &mime_type, int32_t fd) |
341 | { |
342 | QString send_mime_type = mime_type; |
343 | if (send_mime_type == QStringLiteral("text/plain;charset=utf-8" )) { |
344 | // if we get a request on the fallback mime, send the data from the original mime type |
345 | send_mime_type = QStringLiteral("text/plain" ); |
346 | } |
347 | |
348 | QByteArray ba; |
349 | if (m_mimeData->hasImage()) { |
350 | // adapted from QInternalMimeData::renderDataHelper |
351 | if (mime_type == applicationQtXImageLiteral()) { |
352 | QImage image = qvariant_cast<QImage>(v: m_mimeData->imageData()); |
353 | QBuffer buf(&ba); |
354 | buf.open(openMode: QBuffer::WriteOnly); |
355 | // would there not be PNG ?? |
356 | image.save(device: &buf, format: "PNG" ); |
357 | |
358 | } else if (mime_type.startsWith(s: QLatin1String("image/" ))) { |
359 | QImage image = qvariant_cast<QImage>(v: m_mimeData->imageData()); |
360 | QBuffer buf(&ba); |
361 | buf.open(openMode: QBuffer::WriteOnly); |
362 | image.save(device: &buf, format: mime_type.mid(position: mime_type.indexOf(ch: QLatin1Char('/')) + 1).toLatin1().toUpper().data()); |
363 | } |
364 | // end adapted |
365 | } else { |
366 | ba = m_mimeData->data(mimetype: send_mime_type); |
367 | } |
368 | |
369 | QFile c; |
370 | if (!c.open(fd, ioFlags: QFile::WriteOnly, handleFlags: QFile::AutoCloseHandle)) { |
371 | return; |
372 | } |
373 | // Create a sigpipe handler that does nothing, or clients may be forced to terminate |
374 | // if the pipe is closed in the other end. |
375 | struct sigaction action, oldAction; |
376 | action.sa_handler = SIG_IGN; |
377 | sigemptyset(set: &action.sa_mask); |
378 | action.sa_flags = 0; |
379 | sigaction(SIGPIPE, act: &action, oact: &oldAction); |
380 | const int flags = fcntl(fd: fd, F_GETFL, 0); |
381 | if (flags & O_NONBLOCK) { |
382 | fcntl(fd: fd, F_SETFL, flags & ~O_NONBLOCK); // Unset O_NONBLOCK to fix pasting to XWayland windows |
383 | } |
384 | const qint64 written = c.write(data: ba); |
385 | sigaction(SIGPIPE, act: &oldAction, oact: nullptr); |
386 | |
387 | if (written != ba.size()) { |
388 | qWarning() << "Failed to send all clipobard data; sent" << written << "bytes out of" << ba.size(); |
389 | } |
390 | } |
391 | |
392 | void DataControlSource::ext_data_control_source_v1_cancelled() |
393 | { |
394 | Q_EMIT cancelled(); |
395 | } |
396 | |
397 | class DataControlDevice : public QObject, public QtWayland::ext_data_control_device_v1 |
398 | { |
399 | Q_OBJECT |
400 | public: |
401 | DataControlDevice(struct ::ext_data_control_device_v1 *id) |
402 | : QtWayland::ext_data_control_device_v1(id) |
403 | { |
404 | } |
405 | |
406 | ~DataControlDevice() |
407 | { |
408 | destroy(); |
409 | } |
410 | |
411 | void setSelection(std::unique_ptr<DataControlSource> selection); |
412 | QMimeData *receivedSelection() |
413 | { |
414 | return m_receivedSelection.get(); |
415 | } |
416 | QMimeData *selection() |
417 | { |
418 | return m_selection ? m_selection->mimeData() : nullptr; |
419 | } |
420 | |
421 | void setPrimarySelection(std::unique_ptr<DataControlSource> selection); |
422 | QMimeData *receivedPrimarySelection() |
423 | { |
424 | return m_receivedPrimarySelection.get(); |
425 | } |
426 | QMimeData *primarySelection() |
427 | { |
428 | return m_primarySelection ? m_primarySelection->mimeData() : nullptr; |
429 | } |
430 | |
431 | Q_SIGNALS: |
432 | void receivedSelectionChanged(); |
433 | void selectionChanged(); |
434 | |
435 | void receivedPrimarySelectionChanged(); |
436 | void primarySelectionChanged(); |
437 | |
438 | protected: |
439 | void ext_data_control_device_v1_data_offer(struct ::ext_data_control_offer_v1 *id) override |
440 | { |
441 | // this will become memory managed when we retrieve the selection event |
442 | // a compositor calling data_offer without doing that would be a bug |
443 | new DataControlOffer(id); |
444 | } |
445 | |
446 | void ext_data_control_device_v1_selection(struct ::ext_data_control_offer_v1 *id) override |
447 | { |
448 | if (!id) { |
449 | m_receivedSelection.reset(); |
450 | } else { |
451 | auto derivated = QtWayland::ext_data_control_offer_v1::fromObject(object: id); |
452 | auto offer = dynamic_cast<DataControlOffer *>(derivated); // dynamic because of the dual inheritance |
453 | m_receivedSelection.reset(p: offer); |
454 | } |
455 | Q_EMIT receivedSelectionChanged(); |
456 | } |
457 | |
458 | void ext_data_control_device_v1_primary_selection(struct ::ext_data_control_offer_v1 *id) override |
459 | { |
460 | if (!id) { |
461 | m_receivedPrimarySelection.reset(); |
462 | } else { |
463 | auto derivated = QtWayland::ext_data_control_offer_v1::fromObject(object: id); |
464 | auto offer = dynamic_cast<DataControlOffer *>(derivated); // dynamic because of the dual inheritance |
465 | m_receivedPrimarySelection.reset(p: offer); |
466 | } |
467 | Q_EMIT receivedPrimarySelectionChanged(); |
468 | } |
469 | |
470 | private: |
471 | std::unique_ptr<DataControlSource> m_selection; // selection set locally |
472 | std::unique_ptr<DataControlOffer> m_receivedSelection; // latest selection set from externally to here |
473 | |
474 | std::unique_ptr<DataControlSource> m_primarySelection; // selection set locally |
475 | std::unique_ptr<DataControlOffer> m_receivedPrimarySelection; // latest selection set from externally to here |
476 | friend WaylandClipboard; |
477 | }; |
478 | |
479 | void DataControlDevice::setSelection(std::unique_ptr<DataControlSource> selection) |
480 | { |
481 | set_selection(selection->object()); |
482 | |
483 | // Note the previous selection is destroyed after the set_selection request. |
484 | m_selection = std::move(selection); |
485 | connect(sender: m_selection.get(), signal: &DataControlSource::cancelled, context: this, slot: [this]() { |
486 | m_selection.reset(); |
487 | }); |
488 | |
489 | Q_EMIT selectionChanged(); |
490 | } |
491 | |
492 | void DataControlDevice::setPrimarySelection(std::unique_ptr<DataControlSource> selection) |
493 | { |
494 | set_primary_selection(selection->object()); |
495 | |
496 | // Note the previous selection is destroyed after the set_primary_selection request. |
497 | m_primarySelection = std::move(selection); |
498 | connect(sender: m_primarySelection.get(), signal: &DataControlSource::cancelled, context: this, slot: [this]() { |
499 | m_primarySelection.reset(); |
500 | }); |
501 | |
502 | Q_EMIT primarySelectionChanged(); |
503 | } |
504 | class Keyboard; |
505 | // We are binding to Seat/Keyboard manually because we want to react to gaining focus but inside Qt the events are Qt and arrive to late |
506 | class KeyboardFocusWatcher : public QWaylandClientExtensionTemplate<KeyboardFocusWatcher>, public QtWayland::wl_seat |
507 | { |
508 | Q_OBJECT |
509 | public: |
510 | KeyboardFocusWatcher() |
511 | : QWaylandClientExtensionTemplate(5) |
512 | { |
513 | initialize(); |
514 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
515 | auto display = waylandApp->display(); |
516 | // so we get capabilities |
517 | wl_display_roundtrip(display); |
518 | } |
519 | ~KeyboardFocusWatcher() override |
520 | { |
521 | if (isActive()) { |
522 | release(); |
523 | } |
524 | } |
525 | void seat_capabilities(uint32_t capabilities) override |
526 | { |
527 | const bool hasKeyboard = capabilities & capability_keyboard; |
528 | if (hasKeyboard && !m_keyboard) { |
529 | m_keyboard = std::make_unique<Keyboard>(args: get_keyboard(), args&: *this); |
530 | } else if (!hasKeyboard && m_keyboard) { |
531 | m_keyboard.reset(); |
532 | } |
533 | } |
534 | bool hasFocus() const |
535 | { |
536 | return m_focus; |
537 | } |
538 | Q_SIGNALS: |
539 | void keyboardEntered(); |
540 | |
541 | private: |
542 | friend Keyboard; |
543 | bool m_focus = false; |
544 | std::unique_ptr<Keyboard> m_keyboard; |
545 | }; |
546 | |
547 | class Keyboard : public QtWayland::wl_keyboard |
548 | { |
549 | public: |
550 | Keyboard(::wl_keyboard *keyboard, KeyboardFocusWatcher &seat) |
551 | : wl_keyboard(keyboard) |
552 | , m_seat(seat) |
553 | { |
554 | } |
555 | ~Keyboard() |
556 | { |
557 | release(); |
558 | } |
559 | |
560 | private: |
561 | void keyboard_enter([[maybe_unused]] uint32_t serial, [[maybe_unused]] wl_surface *surface, [[maybe_unused]] wl_array *keys) override |
562 | { |
563 | m_seat.m_focus = true; |
564 | Q_EMIT m_seat.keyboardEntered(); |
565 | } |
566 | void keyboard_leave([[maybe_unused]] uint32_t serial, [[maybe_unused]] wl_surface *surface) override |
567 | { |
568 | m_seat.m_focus = false; |
569 | } |
570 | KeyboardFocusWatcher &m_seat; |
571 | }; |
572 | |
573 | WaylandClipboard::WaylandClipboard(QObject *parent) |
574 | : KSystemClipboard(parent) |
575 | , m_keyboardFocusWatcher(new KeyboardFocusWatcher) |
576 | , m_manager(new DataControlDeviceManager) |
577 | { |
578 | connect(sender: m_manager.get(), signal: &DataControlDeviceManager::activeChanged, context: this, slot: [this]() { |
579 | if (m_manager->isActive()) { |
580 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
581 | if (!waylandApp) { |
582 | return; |
583 | } |
584 | auto seat = waylandApp->seat(); |
585 | |
586 | if (!seat) { |
587 | return; |
588 | } |
589 | m_device.reset(p: new DataControlDevice(m_manager->get_data_device(seat))); |
590 | |
591 | connect(sender: m_device.get(), signal: &DataControlDevice::receivedSelectionChanged, context: this, slot: [this]() { |
592 | // When our source is still valid, so the offer is for setting it or we emit changed when it is cancelled |
593 | if (!m_device->selection()) { |
594 | Q_EMIT changed(mode: QClipboard::Clipboard); |
595 | } |
596 | }); |
597 | connect(sender: m_device.get(), signal: &DataControlDevice::selectionChanged, context: this, slot: [this]() { |
598 | Q_EMIT changed(mode: QClipboard::Clipboard); |
599 | }); |
600 | |
601 | connect(sender: m_device.get(), signal: &DataControlDevice::receivedPrimarySelectionChanged, context: this, slot: [this]() { |
602 | // When our source is still valid, so the offer is for setting it or we emit changed when it is cancelled |
603 | if (!m_device->primarySelection()) { |
604 | Q_EMIT changed(mode: QClipboard::Selection); |
605 | } |
606 | }); |
607 | connect(sender: m_device.get(), signal: &DataControlDevice::primarySelectionChanged, context: this, slot: [this]() { |
608 | Q_EMIT changed(mode: QClipboard::Selection); |
609 | }); |
610 | |
611 | } else { |
612 | m_device.reset(); |
613 | } |
614 | }); |
615 | |
616 | m_manager->instantiate(); |
617 | } |
618 | |
619 | WaylandClipboard::~WaylandClipboard() = default; |
620 | |
621 | WaylandClipboard *WaylandClipboard::create(QObject *parent) |
622 | { |
623 | auto clipboard = new WaylandClipboard(parent); |
624 | if (clipboard->isValid()) { |
625 | return clipboard; |
626 | } |
627 | delete clipboard; |
628 | return nullptr; |
629 | } |
630 | |
631 | bool WaylandClipboard::isValid() |
632 | { |
633 | return m_manager && m_manager->isInitialized(); |
634 | } |
635 | |
636 | void WaylandClipboard::setMimeData(QMimeData *mime, QClipboard::Mode mode) |
637 | { |
638 | if (!m_device) { |
639 | return; |
640 | } |
641 | |
642 | // roundtrip to have accurate focus state when losing focus but setting mime data before processing wayland events. |
643 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
644 | auto display = waylandApp->display(); |
645 | wl_display_roundtrip(display); |
646 | |
647 | // If the application is focused, use the normal mechanism so a future paste will not deadlock itselfs |
648 | if (m_keyboardFocusWatcher->hasFocus()) { |
649 | QGuiApplication::clipboard()->setMimeData(data: mime, mode); |
650 | // if we short-circuit the ext_data_device, when we receive the data |
651 | // we cannot identify ourselves as the owner |
652 | // because of that we act like it's a synchronous action to not confuse klipper. |
653 | wl_display_roundtrip(display); |
654 | return; |
655 | } |
656 | // If not, set the clipboard once the app receives focus to avoid the deadlock |
657 | connect(sender: m_keyboardFocusWatcher.get(), signal: &KeyboardFocusWatcher::keyboardEntered, context: this, slot: &WaylandClipboard::gainedFocus, type: Qt::UniqueConnection); |
658 | auto source = std::make_unique<DataControlSource>(args: m_manager->create_data_source(), args&: mime); |
659 | if (mode == QClipboard::Clipboard) { |
660 | m_device->setSelection(std::move(source)); |
661 | } else if (mode == QClipboard::Selection) { |
662 | m_device->setPrimarySelection(std::move(source)); |
663 | } |
664 | } |
665 | |
666 | void WaylandClipboard::gainedFocus() |
667 | { |
668 | disconnect(sender: m_keyboardFocusWatcher.get(), signal: &KeyboardFocusWatcher::keyboardEntered, receiver: this, zero: nullptr); |
669 | // QClipboard takes ownership of the QMimeData so we need to transfer and unset our selections |
670 | if (auto &selection = m_device->m_selection) { |
671 | std::unique_ptr<QMimeData> data = selection->releaseMimeData(); |
672 | selection.reset(); |
673 | QGuiApplication::clipboard()->setMimeData(data: data.release(), mode: QClipboard::Clipboard); |
674 | } |
675 | if (auto &primarySelection = m_device->m_primarySelection) { |
676 | std::unique_ptr<QMimeData> data = primarySelection->releaseMimeData(); |
677 | primarySelection.reset(); |
678 | QGuiApplication::clipboard()->setMimeData(data: data.release(), mode: QClipboard::Selection); |
679 | } |
680 | } |
681 | |
682 | void WaylandClipboard::clear(QClipboard::Mode mode) |
683 | { |
684 | if (!m_device) { |
685 | return; |
686 | } |
687 | if (mode == QClipboard::Clipboard) { |
688 | m_device->set_selection(nullptr); |
689 | m_device->m_selection.reset(); |
690 | } else if (mode == QClipboard::Selection) { |
691 | m_device->set_primary_selection(nullptr); |
692 | m_device->m_primarySelection.reset(); |
693 | } |
694 | } |
695 | |
696 | const QMimeData *WaylandClipboard::mimeData(QClipboard::Mode mode) const |
697 | { |
698 | if (!m_device) { |
699 | return nullptr; |
700 | } |
701 | |
702 | // return our locally set selection if it's not cancelled to avoid copying data to ourselves |
703 | if (mode == QClipboard::Clipboard) { |
704 | if (m_device->selection()) { |
705 | return m_device->selection(); |
706 | } |
707 | // This application owns the clipboard via the regular data_device, use it so we don't block ourselves |
708 | if (QGuiApplication::clipboard()->ownsClipboard()) { |
709 | return QGuiApplication::clipboard()->mimeData(mode); |
710 | } |
711 | return m_device->receivedSelection(); |
712 | } else if (mode == QClipboard::Selection) { |
713 | if (m_device->primarySelection()) { |
714 | return m_device->primarySelection(); |
715 | } |
716 | // This application owns the primary selection via the regular primary_selection_device, use it so we don't block ourselves |
717 | if (QGuiApplication::clipboard()->ownsSelection()) { |
718 | return QGuiApplication::clipboard()->mimeData(mode); |
719 | } |
720 | return m_device->receivedPrimarySelection(); |
721 | } |
722 | return nullptr; |
723 | } |
724 | |
725 | #include "waylandclipboard.moc" |
726 | |