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 "wlrwaylandclipboard_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-wlr-data-control-unstable-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 WlrDataControlDeviceManager : public QWaylandClientExtensionTemplate<WlrDataControlDeviceManager>, public QtWayland::zwlr_data_control_manager_v1 |
67 | { |
68 | Q_OBJECT |
69 | public: |
70 | WlrDataControlDeviceManager() |
71 | : QWaylandClientExtensionTemplate<WlrDataControlDeviceManager>(2) |
72 | { |
73 | } |
74 | |
75 | void instantiate() |
76 | { |
77 | initialize(); |
78 | } |
79 | |
80 | ~WlrDataControlDeviceManager() |
81 | { |
82 | if (isInitialized()) { |
83 | destroy(); |
84 | } |
85 | } |
86 | }; |
87 | |
88 | class WlrDataControlOffer : public QMimeData, public QtWayland::zwlr_data_control_offer_v1 |
89 | { |
90 | Q_OBJECT |
91 | public: |
92 | WlrDataControlOffer(struct ::zwlr_data_control_offer_v1 *id) |
93 | : QtWayland::zwlr_data_control_offer_v1(id) |
94 | { |
95 | } |
96 | |
97 | ~WlrDataControlOffer() |
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 zwlr_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 WlrDataControlOffer::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<WlrDataControlOffer *>(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 WlrDataControlOffer::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: "WlrDataControlOffer: poll() failed for mimeType %s: %s" , qPrintable(mimeType), strerror(errno)); |
265 | return false; |
266 | } |
267 | } else if (ready == 0) { |
268 | qWarning(msg: "WlrDataControlOffer: 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: "WlrDataControlOffer: 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 WlrDataControlSource : public QObject, public QtWayland::zwlr_data_control_source_v1 |
287 | { |
288 | Q_OBJECT |
289 | public: |
290 | WlrDataControlSource(struct ::zwlr_data_control_source_v1 *id, QMimeData *mimeData); |
291 | WlrDataControlSource() = default; |
292 | ~WlrDataControlSource() |
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 zwlr_data_control_source_v1_send(const QString &mime_type, int32_t fd) override; |
311 | void zwlr_data_control_source_v1_cancelled() override; |
312 | |
313 | private: |
314 | std::unique_ptr<QMimeData> m_mimeData; |
315 | }; |
316 | |
317 | WlrDataControlSource::WlrDataControlSource(struct ::zwlr_data_control_source_v1 *id, QMimeData *mimeData) |
318 | : QtWayland::zwlr_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 WlrDataControlSource::zwlr_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 WlrDataControlSource::zwlr_data_control_source_v1_cancelled() |
393 | { |
394 | Q_EMIT cancelled(); |
395 | } |
396 | |
397 | class WlrDataControlDevice : public QObject, public QtWayland::zwlr_data_control_device_v1 |
398 | { |
399 | Q_OBJECT |
400 | public: |
401 | WlrDataControlDevice(struct ::zwlr_data_control_device_v1 *id) |
402 | : QtWayland::zwlr_data_control_device_v1(id) |
403 | { |
404 | } |
405 | |
406 | ~WlrDataControlDevice() |
407 | { |
408 | destroy(); |
409 | } |
410 | |
411 | void setSelection(std::unique_ptr<WlrDataControlSource> 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<WlrDataControlSource> 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 zwlr_data_control_device_v1_data_offer(struct ::zwlr_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 WlrDataControlOffer(id); |
444 | } |
445 | |
446 | void zwlr_data_control_device_v1_selection(struct ::zwlr_data_control_offer_v1 *id) override |
447 | { |
448 | if (!id) { |
449 | m_receivedSelection.reset(); |
450 | } else { |
451 | auto derivated = QtWayland::zwlr_data_control_offer_v1::fromObject(object: id); |
452 | auto offer = dynamic_cast<WlrDataControlOffer *>(derivated); // dynamic because of the dual inheritance |
453 | m_receivedSelection.reset(p: offer); |
454 | } |
455 | Q_EMIT receivedSelectionChanged(); |
456 | } |
457 | |
458 | void zwlr_data_control_device_v1_primary_selection(struct ::zwlr_data_control_offer_v1 *id) override |
459 | { |
460 | if (!id) { |
461 | m_receivedPrimarySelection.reset(); |
462 | } else { |
463 | auto derivated = QtWayland::zwlr_data_control_offer_v1::fromObject(object: id); |
464 | auto offer = dynamic_cast<WlrDataControlOffer *>(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<WlrDataControlSource> m_selection; // selection set locally |
472 | std::unique_ptr<WlrDataControlOffer> m_receivedSelection; // latest selection set from externally to here |
473 | |
474 | std::unique_ptr<WlrDataControlSource> m_primarySelection; // selection set locally |
475 | std::unique_ptr<WlrDataControlOffer> m_receivedPrimarySelection; // latest selection set from externally to here |
476 | friend WlrWaylandClipboard; |
477 | }; |
478 | |
479 | void WlrDataControlDevice::setSelection(std::unique_ptr<WlrDataControlSource> selection) |
480 | { |
481 | m_selection = std::move(selection); |
482 | connect(sender: m_selection.get(), signal: &WlrDataControlSource::cancelled, context: this, slot: [this]() { |
483 | m_selection.reset(); |
484 | }); |
485 | set_selection(m_selection->object()); |
486 | Q_EMIT selectionChanged(); |
487 | } |
488 | |
489 | void WlrDataControlDevice::setPrimarySelection(std::unique_ptr<WlrDataControlSource> selection) |
490 | { |
491 | m_primarySelection = std::move(selection); |
492 | connect(sender: m_primarySelection.get(), signal: &WlrDataControlSource::cancelled, context: this, slot: [this]() { |
493 | m_primarySelection.reset(); |
494 | }); |
495 | |
496 | if (zwlr_data_control_device_v1_get_version(zwlr_data_control_device_v1: object()) >= ZWLR_DATA_CONTROL_DEVICE_V1_SET_PRIMARY_SELECTION_SINCE_VERSION) { |
497 | set_primary_selection(m_primarySelection->object()); |
498 | Q_EMIT primarySelectionChanged(); |
499 | } |
500 | } |
501 | |
502 | class WlrKeyboard; |
503 | // 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 |
504 | class WlrKeyboardFocusWatcher : public QWaylandClientExtensionTemplate<WlrKeyboardFocusWatcher>, public QtWayland::wl_seat |
505 | { |
506 | Q_OBJECT |
507 | public: |
508 | WlrKeyboardFocusWatcher() |
509 | : QWaylandClientExtensionTemplate(5) |
510 | { |
511 | initialize(); |
512 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
513 | auto display = waylandApp->display(); |
514 | // so we get capabilities |
515 | wl_display_roundtrip(display); |
516 | } |
517 | ~WlrKeyboardFocusWatcher() override |
518 | { |
519 | if (isActive()) { |
520 | release(); |
521 | } |
522 | } |
523 | void seat_capabilities(uint32_t capabilities) override |
524 | { |
525 | const bool hasWlrKeyboard = capabilities & capability_keyboard; |
526 | if (hasWlrKeyboard && !m_keyboard) { |
527 | m_keyboard = std::make_unique<WlrKeyboard>(args: get_keyboard(), args&: *this); |
528 | } else if (!hasWlrKeyboard && m_keyboard) { |
529 | m_keyboard.reset(); |
530 | } |
531 | } |
532 | bool hasFocus() const |
533 | { |
534 | return m_focus; |
535 | } |
536 | Q_SIGNALS: |
537 | void keyboardEntered(); |
538 | |
539 | private: |
540 | friend WlrKeyboard; |
541 | bool m_focus = false; |
542 | std::unique_ptr<WlrKeyboard> m_keyboard; |
543 | }; |
544 | |
545 | class WlrKeyboard : public QtWayland::wl_keyboard |
546 | { |
547 | public: |
548 | WlrKeyboard(::wl_keyboard *keyboard, WlrKeyboardFocusWatcher &seat) |
549 | : wl_keyboard(keyboard) |
550 | , m_seat(seat) |
551 | { |
552 | } |
553 | ~WlrKeyboard() |
554 | { |
555 | release(); |
556 | } |
557 | |
558 | private: |
559 | void keyboard_enter([[maybe_unused]] uint32_t serial, [[maybe_unused]] wl_surface *surface, [[maybe_unused]] wl_array *keys) override |
560 | { |
561 | m_seat.m_focus = true; |
562 | Q_EMIT m_seat.keyboardEntered(); |
563 | } |
564 | void keyboard_leave([[maybe_unused]] uint32_t serial, [[maybe_unused]] wl_surface *surface) override |
565 | { |
566 | m_seat.m_focus = false; |
567 | } |
568 | WlrKeyboardFocusWatcher &m_seat; |
569 | }; |
570 | |
571 | WlrWaylandClipboard::WlrWaylandClipboard(QObject *parent) |
572 | : KSystemClipboard(parent) |
573 | , m_keyboardFocusWatcher(new WlrKeyboardFocusWatcher) |
574 | , m_manager(new WlrDataControlDeviceManager) |
575 | { |
576 | connect(sender: m_manager.get(), signal: &WlrDataControlDeviceManager::activeChanged, context: this, slot: [this]() { |
577 | if (m_manager->isActive()) { |
578 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
579 | if (!waylandApp) { |
580 | return; |
581 | } |
582 | auto seat = waylandApp->seat(); |
583 | |
584 | if (!seat) { |
585 | return; |
586 | } |
587 | m_device.reset(p: new WlrDataControlDevice(m_manager->get_data_device(seat))); |
588 | |
589 | connect(sender: m_device.get(), signal: &WlrDataControlDevice::receivedSelectionChanged, context: this, slot: [this]() { |
590 | // When our source is still valid, so the offer is for setting it or we emit changed when it is cancelled |
591 | if (!m_device->selection()) { |
592 | Q_EMIT changed(mode: QClipboard::Clipboard); |
593 | } |
594 | }); |
595 | connect(sender: m_device.get(), signal: &WlrDataControlDevice::selectionChanged, context: this, slot: [this]() { |
596 | Q_EMIT changed(mode: QClipboard::Clipboard); |
597 | }); |
598 | |
599 | connect(sender: m_device.get(), signal: &WlrDataControlDevice::receivedPrimarySelectionChanged, context: this, slot: [this]() { |
600 | // When our source is still valid, so the offer is for setting it or we emit changed when it is cancelled |
601 | if (!m_device->primarySelection()) { |
602 | Q_EMIT changed(mode: QClipboard::Selection); |
603 | } |
604 | }); |
605 | connect(sender: m_device.get(), signal: &WlrDataControlDevice::primarySelectionChanged, context: this, slot: [this]() { |
606 | Q_EMIT changed(mode: QClipboard::Selection); |
607 | }); |
608 | |
609 | } else { |
610 | m_device.reset(); |
611 | } |
612 | }); |
613 | |
614 | m_manager->instantiate(); |
615 | } |
616 | |
617 | WlrWaylandClipboard::~WlrWaylandClipboard() = default; |
618 | |
619 | WlrWaylandClipboard *WlrWaylandClipboard::create(QObject *parent) |
620 | { |
621 | auto clipboard = new WlrWaylandClipboard(parent); |
622 | if (clipboard->isValid()) { |
623 | return clipboard; |
624 | } |
625 | delete clipboard; |
626 | return nullptr; |
627 | } |
628 | |
629 | bool WlrWaylandClipboard::isValid() |
630 | { |
631 | return m_manager && m_manager->isInitialized(); |
632 | } |
633 | |
634 | void WlrWaylandClipboard::setMimeData(QMimeData *mime, QClipboard::Mode mode) |
635 | { |
636 | if (!m_device) { |
637 | return; |
638 | } |
639 | |
640 | // roundtrip to have accurate focus state when losing focus but setting mime data before processing wayland events. |
641 | auto waylandApp = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>(); |
642 | auto display = waylandApp->display(); |
643 | wl_display_roundtrip(display); |
644 | |
645 | // If the application is focused, use the normal mechanism so a future paste will not deadlock itselfs |
646 | if (m_keyboardFocusWatcher->hasFocus()) { |
647 | QGuiApplication::clipboard()->setMimeData(data: mime, mode); |
648 | // if we short-circuit the wlr_data_device, when we receive the data |
649 | // we cannot identify ourselves as the owner |
650 | // because of that we act like it's a synchronous action to not confuse klipper. |
651 | wl_display_roundtrip(display); |
652 | return; |
653 | } |
654 | // If not, set the clipboard once the app receives focus to avoid the deadlock |
655 | connect(sender: m_keyboardFocusWatcher.get(), signal: &WlrKeyboardFocusWatcher::keyboardEntered, context: this, slot: &WlrWaylandClipboard::gainedFocus, type: Qt::UniqueConnection); |
656 | auto source = std::make_unique<WlrDataControlSource>(args: m_manager->create_data_source(), args&: mime); |
657 | if (mode == QClipboard::Clipboard) { |
658 | m_device->setSelection(std::move(source)); |
659 | } else if (mode == QClipboard::Selection) { |
660 | m_device->setPrimarySelection(std::move(source)); |
661 | } |
662 | } |
663 | |
664 | void WlrWaylandClipboard::gainedFocus() |
665 | { |
666 | disconnect(sender: m_keyboardFocusWatcher.get(), signal: &WlrKeyboardFocusWatcher::keyboardEntered, receiver: this, zero: nullptr); |
667 | // QClipboard takes ownership of the QMimeData so we need to transfer and unset our selections |
668 | if (auto &selection = m_device->m_selection) { |
669 | std::unique_ptr<QMimeData> data = selection->releaseMimeData(); |
670 | selection.reset(); |
671 | QGuiApplication::clipboard()->setMimeData(data: data.release(), mode: QClipboard::Clipboard); |
672 | } |
673 | if (auto &primarySelection = m_device->m_primarySelection) { |
674 | std::unique_ptr<QMimeData> data = primarySelection->releaseMimeData(); |
675 | primarySelection.reset(); |
676 | QGuiApplication::clipboard()->setMimeData(data: data.release(), mode: QClipboard::Selection); |
677 | } |
678 | } |
679 | |
680 | void WlrWaylandClipboard::clear(QClipboard::Mode mode) |
681 | { |
682 | if (!m_device) { |
683 | return; |
684 | } |
685 | if (mode == QClipboard::Clipboard) { |
686 | m_device->set_selection(nullptr); |
687 | m_device->m_selection.reset(); |
688 | } else if (mode == QClipboard::Selection) { |
689 | if (zwlr_data_control_device_v1_get_version(zwlr_data_control_device_v1: m_device->object()) >= ZWLR_DATA_CONTROL_DEVICE_V1_SET_PRIMARY_SELECTION_SINCE_VERSION) { |
690 | m_device->set_primary_selection(nullptr); |
691 | m_device->m_primarySelection.reset(); |
692 | } |
693 | } |
694 | } |
695 | |
696 | const QMimeData *WlrWaylandClipboard::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 "wlrwaylandclipboard.moc" |
726 | |